tests for async components

This commit is contained in:
Evan You 2015-05-12 18:51:05 -04:00
parent d869ba0550
commit dffd4c7a0d
8 changed files with 306 additions and 11 deletions

View File

@ -26,6 +26,7 @@ module.exports = function (grunt) {
options: {
frameworks: ['jasmine', 'commonjs'],
files: [
'test/unit/lib/util.js',
'test/unit/lib/jquery.js',
'src/**/*.js',
'test/unit/specs/**/*.js'

View File

@ -36,8 +36,10 @@ module.exports = {
// extract inline template as a DocumentFragment
this.template = _.extractContent(this.el, true)
}
// pending callback for async component resolution
this._pendingCb = null
// component resolution related state
this._pendingCb =
this.ctorId =
this.Ctor = null
// if static, build right now.
if (!this._isDynamicLiteral) {
this.resolveCtor(this.expression, _.bind(function () {

View File

@ -106,6 +106,7 @@ module.exports = {
copy._asComponent = false
this._linkFn = compile(this.template, copy)
} else {
this.Ctor = null
this.asComponent = true
// check inline-template
if (this._checkParam('inline-template') !== null) {
@ -181,6 +182,7 @@ module.exports = {
'Async resolution is not supported for v-repeat ' +
'+ dynamic component. (component: ' + id + ')'
)
return _.Vue
}
return Ctor
},

View File

@ -37,10 +37,10 @@ exports._resolveComponent = function (id, cb) {
if (factory.resolved) {
// cached
cb(factory.resolved)
} else if (factory.pending) {
} else if (factory.requested) {
factory.pendingCallbacks.push(cb)
} else {
factory.pending = true
factory.requested = true
var cbs = factory.pendingCallbacks = [cb]
factory(function resolve (res) {
if (_.isPlainObject(res)) {
@ -48,7 +48,6 @@ exports._resolveComponent = function (id, cb) {
}
// cache resolved
factory.resolved = res
factory.pending = false
// invoke callbacks
for (var i = 0, l = cbs.length; i < l; i++) {
cbs[i](res)

View File

@ -27,6 +27,7 @@
"casper": true,
"DocumentFragment": true,
"jQuery": true,
"$": true
"$": true,
"hasWarned": true
}
}

7
test/unit/lib/util.js Normal file
View File

@ -0,0 +1,7 @@
var scope = typeof window === 'undefined'
? global
: window
scope.hasWarned = function (_, msg) {
return _.warn.calls.argsFor(0)[0].indexOf(msg) > -1
}

View File

@ -9,6 +9,7 @@
<script src="lib/jasmine.js"></script>
<script src="lib/jasmine-html.js"></script>
<script src="lib/boot.js"></script>
<script src="lib/util.js"></script>
<script src="specs.js"></script>
</head>
<body>

View File

@ -1,17 +1,299 @@
var Vue = require('../../../src/vue')
var _ = Vue.util
describe('Async components', function () {
var el
beforeEach(function () {
el = document.createElement('div')
document.body.appendChild(el)
spyOn(_, 'warn')
})
afterEach(function () {
document.body.removeChild(el)
})
describe('v-component', function () {
// - normal
// - dynamic
// - nested component caching
// - invalidate pending callback on teardown
// - avoid duplicate requests
it('normal', function (done) {
var vm = new Vue({
el: el,
template: '<div v-component="test"></div>',
components: {
test: function (resolve) {
setTimeout(function () {
resolve({
template: 'ok'
})
next()
}, 0)
}
}
})
function next () {
expect(el.textContent).toBe('ok')
done()
}
})
it('dynamic', function (done) {
var vm = new Vue({
el: el,
template: '<div v-component="{{view}}"></div>',
data: {
view: 'a'
},
components: {
a: function (resolve) {
setTimeout(function () {
resolve({
template: 'A'
})
step1()
}, 0)
},
b: function (resolve) {
setTimeout(function () {
resolve({
template: 'B'
})
step2()
}, 0)
}
}
})
var aCalled = false
function step1 () {
// ensure A is resolved only once
expect(aCalled).toBe(false)
aCalled = true
expect(el.textContent).toBe('A')
vm.view = 'b'
}
function step2 () {
expect(el.textContent).toBe('B')
vm.view = 'a'
_.nextTick(function () {
expect(el.textContent).toBe('A')
done()
})
}
})
it('invalidate pending on dynamic switch', function (done) {
var vm = new Vue({
el: el,
template: '<div v-component="{{view}}"></div>',
data: {
view: 'a'
},
components: {
a: function (resolve) {
setTimeout(function () {
resolve({
template: 'A'
})
step1()
}, 100)
},
b: function (resolve) {
setTimeout(function () {
resolve({
template: 'B'
})
step2()
}, 200)
}
}
})
expect(el.textContent).toBe('')
vm.view = 'b'
function step1 () {
// called after A resolves, but A should have been
// invalidated so not cotrId should be set
expect(vm._directives[0].ctorId).toBe(null)
}
function step2 () {
// B should resolve successfully
expect(el.textContent).toBe('B')
done()
}
})
it('invalidate pending on teardown', function (done) {
var vm = new Vue({
el: el,
template: '<div v-component="a"></div>',
data: {
view: 'a'
},
components: {
a: function (resolve) {
setTimeout(function () {
resolve({
template: 'A'
})
next()
}, 100)
}
}
})
expect(el.textContent).toBe('')
// cache directive isntance before destroy
var dir = vm._directives[0]
vm.$destroy()
function next () {
// called after A resolves, but A should have been
// invalidated so not cotrId should be set
expect(dir.ctorId).toBe(null)
done()
}
})
it('avoid duplicate requests', function (done) {
var factoryCallCount = 0
var instanceCount = 0
var vm = new Vue({
el: el,
template:
'<div v-component="a"></div>' +
'<div v-component="a"></div>',
components: {
a: factory
}
})
function factory (resolve) {
factoryCallCount++
setTimeout(function () {
resolve({
template: 'A',
created: function () {
instanceCount++
}
})
next()
}, 0)
}
function next () {
expect(factoryCallCount).toBe(1)
expect(el.textContent).toBe('AA')
expect(instanceCount).toBe(2)
done()
}
})
})
describe('v-repeat', function () {
// - normal
// - invalidate on teardown
// - warn for dynamic
it('normal', function (done) {
var vm = new Vue({
el: el,
template: '<div v-repeat="list" v-component="test"></div>',
data: {
list: [1, 2, 3]
},
components: {
test: function (resolve) {
setTimeout(function () {
resolve({
template: '{{$value}}'
})
next()
}, 0)
}
}
})
function next () {
expect(el.textContent).toBe('123')
done()
}
})
it('only resolve once', function (done) {
var vm = new Vue({
el: el,
template: '<div v-repeat="list" v-component="test"></div>',
data: {
list: [1, 2, 3]
},
components: {
test: function (resolve) {
setTimeout(function () {
resolve({
template: '{{$value}}'
})
next()
}, 100)
}
}
})
// hijack realUpdate - this should only be called once
// spyOn doesn't work here
var update = vm._directives[0].realUpdate
var callCount = 0
vm._directives[0].realUpdate = function () {
callCount++
update.apply(this, arguments)
}
vm.list = [2, 3, 4]
function next () {
expect(el.textContent).toBe('234')
expect(callCount).toBe(1)
done()
}
})
it('invalidate on teardown', function (done) {
var vm = new Vue({
el: el,
template: '<div v-repeat="list" v-component="test"></div>',
data: {
list: [1, 2, 3]
},
components: {
test: function (resolve) {
setTimeout(function () {
resolve({
template: '{{$value}}'
})
next()
}, 0)
}
}
})
var dir = vm._directives[0]
vm.$destroy()
function next () {
expect(el.textContent).toBe('')
expect(dir.Ctor).toBe(null)
done()
}
})
it('warn when used with dynamic v-repeat', function () {
var vm = new Vue({
el: el,
template: '<div v-repeat="list" v-component="{{c}}"></div>',
data: {
list: [1, 2, 3],
c: 'test'
},
components: {
test: function (resolve) {
setTimeout(function () {
resolve({
template: '{{$value}}'
})
}, 0)
}
}
})
expect(hasWarned(_, 'Async resolution is not supported')).toBe(true)
})
})
})