ソースを参照

update unit tests convention + basic component tests

Evan You 10 年 前
コミット
6593be9f75

+ 1 - 1
src/compiler/codegen.js

@@ -41,7 +41,7 @@ function genElement (el) {
     return genRender(el)
   } else if (el.tag === 'slot') {
     return genSlot(el)
-  } else if (el.tag === 'component') {
+  } else if (el.component) {
     return genComponent(el)
   } else {
     // if the element is potentially a component,

+ 3 - 2
src/compiler/parser/index.js

@@ -278,8 +278,9 @@ function processSlot (el) {
 }
 
 function processComponent (el) {
-  if (el.tag === 'component') {
-    el.component = getBindingAttr(el, 'is')
+  const isBinding = getBindingAttr(el, 'is')
+  if (isBinding) {
+    el.component = isBinding
   }
   if (getAndRemoveAttr(el, 'inline-template') != null) {
     el.inlineTemplate = true

+ 2 - 1
src/core/instance/render.js

@@ -1,4 +1,5 @@
 import createElement from '../vdom/create-element'
+import { emptyVNode } from '../vdom/vnode'
 import { flatten } from '../vdom/helpers'
 import { bind, isArray, isObject, renderString } from 'shared/util'
 import { resolveAsset, nextTick } from '../util/index'
@@ -38,7 +39,7 @@ export function renderMixin (Vue) {
       resolveSlots(this, _renderChildren)
     }
     // render self
-    const vnode = render.call(this._renderProxy)
+    const vnode = render.call(this._renderProxy) || emptyVNode
     // set parent
     vnode.parent = _parentVnode
     // restore render state

+ 5 - 1
src/core/vdom/create-element.js

@@ -1,4 +1,4 @@
-import VNode from './vnode'
+import VNode, { emptyVNode } from './vnode'
 import config from '../config'
 import { createComponent } from './create-component'
 import { flatten } from './helpers'
@@ -8,6 +8,10 @@ import { warn, resolveAsset } from '../util/index'
 export default function createElement (tag, data, children, namespace) {
   const context = this
   const parent = renderState.activeInstance
+  if (!tag) {
+    // in case of component :is set to falsy value
+    return emptyVNode
+  }
   if (typeof tag === 'string') {
     let Ctor
     if (config.isReservedTag(tag)) {

+ 1 - 1
src/core/vdom/patch.js

@@ -348,7 +348,7 @@ export function createPatchFunction (backend) {
       if (sameVnode(oldVnode, vnode)) {
         patchVnode(oldVnode, vnode, insertedVnodeQueue)
       } else {
-        if (isUndef(oldVnode.tag)) {
+        if (isDef(oldVnode.nodeType)) {
           // mounting to a real element
           // check if this is server-rendered content and if we can perform
           // a successful hydration.

+ 2 - 0
src/core/vdom/vnode.js

@@ -10,3 +10,5 @@ export default function VNode (tag, data, children, text, elm, ns, context) {
     key: data && data.key
   }
 }
+
+export const emptyVNode = VNode(undefined, undefined, undefined, '')

+ 2 - 1
src/entries/web-runtime-with-compiler.js

@@ -29,7 +29,8 @@ Vue.prototype.$mount = function (el) {
     if (template) {
       const { render, staticRenderFns } = compileToFunctions(template, {
         preserveWhitespace: config.preserveWhitespace,
-        delimiters: options.delimiters
+        delimiters: options.delimiters,
+        warn
       })
       options.render = render
       options.staticRenderFns = staticRenderFns

+ 0 - 0
test/unit/features/component/.gitkeep


+ 229 - 0
test/unit/features/component/component.spec.js

@@ -0,0 +1,229 @@
+import Vue from 'vue'
+
+describe('Component', () => {
+  it('static', () => {
+    const vm = new Vue({
+      template: '<test></test>',
+      components: {
+        test: {
+          data () {
+            return { a: 123 }
+          },
+          template: '<span>{{a}}</span>'
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.tagName).toBe('SPAN')
+    expect(vm.$el.innerHTML).toBe('123')
+  })
+
+  it('using component in restricted elements', () => {
+    const vm = new Vue({
+      template: '<div><table><tbody><test></test></tbody></table></div>',
+      components: {
+        test: {
+          data () {
+            return { a: 123 }
+          },
+          template: '<tr><td>{{a}}</td></tr>'
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.innerHTML).toBe('<table><tbody><tr><td>123</td></tr></tbody></table>')
+  })
+
+  it('"is" attribute', () => {
+    const vm = new Vue({
+      template: '<div><table><tbody><tr is="test"></tr></tbody></table></div>',
+      components: {
+        test: {
+          data () {
+            return { a: 123 }
+          },
+          template: '<tr><td>{{a}}</td></tr>'
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.innerHTML).toBe('<table><tbody><tr><td>123</td></tr></tbody></table>')
+  })
+
+  it('inline-template', () => {
+    const vm = new Vue({
+      template: '<div><test inline-template><span>{{a}}</span></test></div>',
+      data: {
+        a: 'parent'
+      },
+      components: {
+        test: {
+          data () {
+            return { a: 'child' }
+          }
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.innerHTML).toBe('<span>child</span>')
+  })
+
+  it('fragment instance warning', () => {
+    const vm = new Vue({
+      template: '<test></test>',
+      components: {
+        test: {
+          data () {
+            return { a: 123, b: 234 }
+          },
+          template: '<p>{{a}}</p><p>{{b}}</p>'
+        }
+      }
+    })
+    vm.$mount()
+    expect('Component template should contain exactly one root element').toHaveBeenWarned()
+  })
+
+  it('dynamic', done => {
+    const vm = new Vue({
+      template: '<component :is="view" :view="view"></component>',
+      data: {
+        view: 'view-a'
+      },
+      components: {
+        'view-a': {
+          template: '<div>foo</div>',
+          data () {
+            return { view: 'a' }
+          }
+        },
+        'view-b': {
+          template: '<div>bar</div>',
+          data () {
+            return { view: 'b' }
+          }
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.outerHTML).toBe('<div view="view-a">foo</div>')
+    vm.view = 'view-b'
+    waitForUpdate(() => {
+      expect(vm.$el.outerHTML).toBe('<div view="view-b">bar</div>')
+      vm.view = ''
+    })
+    .then(() => {
+      expect(vm.$el.nodeType).toBe(3)
+      expect(vm.$el.data).toBe('')
+      done()
+    })
+    .catch(done)
+  })
+
+  it(':is using raw component constructor', () => {
+    const vm = new Vue({
+      template:
+        '<div>' +
+          '<component :is="$options.components.test"></component>' +
+          '<component :is="$options.components.async"></component>' +
+        '</div>',
+      components: {
+        test: {
+          template: '<span>foo</span>'
+        },
+        async: function (resolve) {
+          resolve({
+            template: '<span>bar</span>'
+          })
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
+  })
+
+  it('should compile parent template directives & content in parent scope', done => {
+    const vm = new Vue({
+      data: {
+        ok: false,
+        message: 'hello'
+      },
+      template: '<test v-show="ok">{{message}}</test>',
+      components: {
+        test: {
+          template: '<div><slot></slot> {{message}}</div>',
+          data () {
+            return {
+              message: 'world'
+            }
+          }
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.style.display).toBe('none')
+    expect(vm.$el.textContent).toBe('hello world')
+    vm.ok = true
+    vm.message = 'bye'
+    waitForUpdate(() => {
+      expect(vm.$el.style.display).toBe('')
+      expect(vm.$el.textContent).toBe('bye world')
+      done()
+    }).catch(done)
+  })
+
+  it('parent content + v-if', done => {
+    const vm = new Vue({
+      data: {
+        ok: false,
+        message: 'hello'
+      },
+      template: '<test v-if="ok">{{message}}</test>',
+      components: {
+        test: {
+          template: '<div><slot></slot> {{message}}</div>',
+          data () {
+            return {
+              message: 'world'
+            }
+          }
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.textContent).toBe('')
+    expect(vm.$children.length).toBe(0)
+    vm.ok = true
+    waitForUpdate(() => {
+      expect(vm.$children.length).toBe(1)
+      expect(vm.$el.textContent).toBe('hello world')
+      done()
+    })
+    .catch(done)
+  })
+
+  it('props', () => {
+    const vm = new Vue({
+      data: {
+        list: [{ a: 1 }, { a: 2 }]
+      },
+      template: '<test :collection="list"></test>',
+      components: {
+        test: {
+          template: '<ul><li v-for="item in collection">{{item.a}}</li></ul>',
+          props: ['collection']
+        }
+      }
+    })
+    vm.$mount()
+    expect(vm.$el.outerHTML).toBe('<ul><li>1</li><li>2</li></ul>')
+  })
+
+  it('not found component should not throw', () => {
+    expect(function () {
+      new Vue({
+        template: '<div is="non-existent"></div>'
+      })
+    }).not.toThrow()
+  })
+})

+ 5 - 5
test/unit/features/directives/bind.spec.js

@@ -3,10 +3,10 @@ import Vue from 'vue'
 describe('Directive v-bind', () => {
   it('normal attr', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span :test="foo">hello</span></div>',
       data: { foo: 'ok' }
     })
+    vm.$mount()
     expect(vm.$el.firstChild.getAttribute('test')).toBe('ok')
     vm.foo = 'again'
     waitForUpdate(() => {
@@ -29,7 +29,6 @@ describe('Directive v-bind', () => {
 
   it('should set property for input value', done => {
     const vm = new Vue({
-      el: '#app',
       template: `
         <div>
           <input type="text" :value="foo">
@@ -41,6 +40,7 @@ describe('Directive v-bind', () => {
         bar: false
       }
     })
+    vm.$mount()
     expect(vm.$el.firstChild.value).toBe('ok')
     expect(vm.$el.lastChild.checked).toBe(false)
     vm.bar = true
@@ -52,12 +52,12 @@ describe('Directive v-bind', () => {
 
   it('xlink', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<svg><a :xlink:special="foo"></a></svg>',
       data: {
         foo: 'ok'
       }
     })
+    vm.$mount()
     const xlinkNS = 'http://www.w3.org/1999/xlink'
     expect(vm.$el.firstChild.getAttributeNS(xlinkNS, 'special')).toBe('ok')
     vm.foo = 'again'
@@ -75,10 +75,10 @@ describe('Directive v-bind', () => {
 
   it('enumrated attr', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span :draggable="foo">hello</span></div>',
       data: { foo: true }
     })
+    vm.$mount()
     expect(vm.$el.firstChild.getAttribute('draggable')).toBe('true')
     vm.foo = 'again'
     waitForUpdate(() => {
@@ -101,10 +101,10 @@ describe('Directive v-bind', () => {
 
   it('boolean attr', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span :disabled="foo">hello</span></div>',
       data: { foo: true }
     })
+    vm.$mount()
     expect(vm.$el.firstChild.getAttribute('disabled')).toBe('disabled')
     vm.foo = 'again'
     waitForUpdate(() => {

+ 3 - 3
test/unit/features/directives/html.spec.js

@@ -3,28 +3,28 @@ import Vue from 'vue'
 describe('Directive v-html', () => {
   it('should render html', () => {
     const vm = new Vue({
-      el: '#app',
       template: '<div v-html="a"></div>',
       data: { a: 'hello' }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('hello')
   })
 
   it('should encode html entities', () => {
     const vm = new Vue({
-      el: '#app',
       template: '<div v-html="a"></div>',
       data: { a: '<span></span>' }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span></span>')
   })
 
   it('should support all value types', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div v-html="a"></div>',
       data: { a: false }
     })
+    vm.$mount()
     waitForUpdate(() => {
       expect(vm.$el.innerHTML).toBe('false')
       vm.a = []

+ 6 - 6
test/unit/features/directives/if.spec.js

@@ -3,28 +3,28 @@ import Vue from 'vue'
 describe('Directive v-if', () => {
   it('should check if value is truthy', () => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span v-if="foo">hello</span></div>',
       data: { foo: true }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span>hello</span>')
   })
 
   it('should check if value is falsy', () => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span v-if="foo">hello</span></div>',
       data: { foo: false }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('')
   })
 
   it('should update if value changed', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div><span v-if="foo">hello</span></div>',
       data: { foo: true }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span>hello</span>')
     vm.foo = false
     waitForUpdate(() => {
@@ -57,7 +57,6 @@ describe('Directive v-if', () => {
 
   it('should work well with v-else', done => {
     const vm = new Vue({
-      el: '#app',
       template: `
         <div>
           <span v-if="foo">hello</span>
@@ -66,6 +65,7 @@ describe('Directive v-if', () => {
       `,
       data: { foo: true }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span>hello</span>')
     vm.foo = false
     waitForUpdate(() => {
@@ -97,7 +97,6 @@ describe('Directive v-if', () => {
 
   it('should work well with v-for', done => {
     const vm = new Vue({
-      el: '#app',
       template: `
         <div>
           <span v-for="item,i in list" v-if="item.value">{{i}}</span>
@@ -111,6 +110,7 @@ describe('Directive v-if', () => {
         ]
       }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span>0</span><span>2</span>')
     vm.list[0].value = false
     waitForUpdate(() => {
@@ -127,7 +127,6 @@ describe('Directive v-if', () => {
 
   it('should work well with v-for and v-else', done => {
     const vm = new Vue({
-      el: '#app',
       template: `
         <div>
           <span v-for="item,i in list" v-if="item.value">hello</span>
@@ -142,6 +141,7 @@ describe('Directive v-if', () => {
         ]
       }
     })
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('<span>hello</span><span>bye</span><span>hello</span>')
     vm.list[0].value = false
     waitForUpdate(() => {

+ 15 - 22
test/unit/features/directives/on.spec.js

@@ -9,13 +9,12 @@ function trigger (target, event, process) {
 }
 
 describe('Directive v-on', () => {
-  let vm, spy, spy2
+  let vm, spy, spy2, el
 
   beforeEach(() => {
     spy = jasmine.createSpy()
     spy2 = jasmine.createSpy()
-    const el = document.createElement('div')
-    el.id = 'app'
+    el = document.createElement('div')
     document.body.appendChild(el)
   })
 
@@ -25,12 +24,11 @@ describe('Directive v-on', () => {
 
   it('should bind event to a method', () => {
     vm = new Vue({
-      el: '#app',
+      el,
       template: '<div v-on:click="foo"></div>',
       methods: { foo: spy }
     })
-    const el = vm.$el
-    trigger(el, 'click')
+    trigger(vm.$el, 'click')
     expect(spy.calls.count()).toBe(1)
 
     const args = spy.calls.allArgs()
@@ -40,12 +38,11 @@ describe('Directive v-on', () => {
 
   it('should bind event to a inline method', () => {
     vm = new Vue({
-      el: '#app',
+      el,
       template: '<div v-on:click="foo(1,2,3,$event)"></div>',
       methods: { foo: spy }
     })
-    const el = vm.$el
-    trigger(el, 'click')
+    trigger(vm.$el, 'click')
     expect(spy.calls.count()).toBe(1)
 
     const args = spy.calls.allArgs()
@@ -59,32 +56,30 @@ describe('Directive v-on', () => {
 
   it('should support shorthand', () => {
     vm = new Vue({
-      el: '#app',
+      el,
       template: '<a href="#test" @click.prevent="foo"></a>',
       methods: { foo: spy }
     })
-    const el = vm.$el
-    trigger(el, 'click')
+    trigger(vm.$el, 'click')
     expect(spy.calls.count()).toBe(1)
   })
 
   it('should support stop propagation', () => {
     vm = new Vue({
-      el: '#app',
+      el,
       template: `
         <div @click.stop="foo"></div>
       `,
       methods: { foo: spy }
     })
     const hash = window.location.hash
-    const el = vm.$el
-    trigger(el, 'click')
+    trigger(vm.$el, 'click')
     expect(window.location.hash).toBe(hash)
   })
 
   it('should support prevent default', () => {
     vm = new Vue({
-      el: '#app',
+      el,
       template: `
         <div @click="bar">
           <div @click.stop="foo"></div>
@@ -92,8 +87,7 @@ describe('Directive v-on', () => {
       `,
       methods: { foo: spy, bar: spy2 }
     })
-    const el = vm.$el
-    trigger(el.firstChild, 'click')
+    trigger(vm.$el.firstChild, 'click')
     expect(spy).toHaveBeenCalled()
     expect(spy2).not.toHaveBeenCalled()
   })
@@ -101,7 +95,7 @@ describe('Directive v-on', () => {
   it('should support capture', () => {
     const callOrder = []
     vm = new Vue({
-      el: '#app',
+      el,
       template: `
         <div @click.capture="foo">
           <div @click="bar"></div>
@@ -112,8 +106,7 @@ describe('Directive v-on', () => {
         bar () { callOrder.push(2) }
       }
     })
-    const el = vm.$el
-    trigger(el.firstChild, 'click')
+    trigger(vm.$el.firstChild, 'click')
     expect(callOrder.toString()).toBe('1,2')
   })
 
@@ -122,7 +115,7 @@ describe('Directive v-on', () => {
       template: '<span>Hello</span>'
     })
     vm = new Vue({
-      el: '#app',
+      el,
       template: '<bar @custom="foo"></bar>',
       methods: { foo: spy }
     })

+ 4 - 8
test/unit/features/directives/once.spec.js

@@ -3,11 +3,10 @@ import Vue from 'vue'
 describe('Directive v-once', () => {
   it('should not rerender component', done => {
     const vm = new Vue({
-      el: '#app',
       template: '<div v-once>{{ a }}</div>',
       data: { a: 'hello' }
     })
-
+    vm.$mount()
     expect(vm.$el.innerHTML).toBe('hello')
     vm.a = 'world'
     waitForUpdate(() => {
@@ -18,7 +17,6 @@ describe('Directive v-once', () => {
 
   it('should not rerender self and child component', done => {
     const vm = new Vue({
-      el: '#app',
       template: `<div v-once>
                   <span>{{ a }}</span>
                   <item :b="a"></item>
@@ -31,7 +29,7 @@ describe('Directive v-once', () => {
         }
       }
     })
-
+    vm.$mount()
     expect(vm.$children.length).toBe(0)
     expect(vm.$el.innerHTML)
       .toBe('<span>hello</span><div>hello</div>')
@@ -45,7 +43,6 @@ describe('Directive v-once', () => {
 
   it('should rerender parent but not self', done => {
     const vm = new Vue({
-      el: '#app',
       template: `<div>
                   <span>{{ a }}</span>
                   <item v-once :b="a"></item>
@@ -58,7 +55,7 @@ describe('Directive v-once', () => {
         }
       }
     })
-
+    vm.$mount()
     expect(vm.$children.length).toBe(0)
     expect(vm.$el.innerHTML)
       .toBe('<span>hello</span><div>hello</div>')
@@ -72,7 +69,6 @@ describe('Directive v-once', () => {
 
   it('should not rerender static sub nodes', done => {
     const vm = new Vue({
-      el: '#app',
       template: `<div>
                   <span v-once>{{ a }}</span>
                   <item :b="a"></item>
@@ -89,7 +85,7 @@ describe('Directive v-once', () => {
         }
       }
     })
-
+    vm.$mount()
     expect(vm.$el.innerHTML)
       .toBe('<span>hello</span><div>hello</div><span>?</span>')
     vm.a = 'world'