props tests

This commit is contained in:
Evan You 2016-05-31 15:15:01 -04:00
parent 3db3ca5623
commit 6a48b35473
4 changed files with 374 additions and 10 deletions

View File

@ -57,10 +57,19 @@ function initData (vm: Component) {
} }
// proxy data on instance // proxy data on instance
const keys = Object.keys(data) const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length let i = keys.length
while (i--) { while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else {
proxy(vm, keys[i]) proxy(vm, keys[i])
} }
}
// observe data // observe data
observe(data) observe(data)
data.__ob__ && data.__ob__.vmCount++ data.__ob__ && data.__ob__.vmCount++

View File

@ -1,6 +1,6 @@
/* @flow */ /* @flow */
import { hasOwn, isObject, isPlainObject } from 'shared/util' import { hasOwn, isObject, isPlainObject, capitalize, hyphenate } from 'shared/util'
import { observe, observerState } from '../observer/index' import { observe, observerState } from '../observer/index'
import { warn } from './debug' import { warn } from './debug'
@ -14,8 +14,16 @@ type PropOptions = {
export function validateProp (vm: Component, key: string, propsData: ?Object): any { export function validateProp (vm: Component, key: string, propsData: ?Object): any {
if (!vm.$options.props || !propsData) return if (!vm.$options.props || !propsData) return
const prop = vm.$options.props[key] const prop = vm.$options.props[key]
const absent = hasOwn(propsData, key) const absent = !hasOwn(propsData, key)
let value = propsData[key] let value = propsData[key]
// handle boolean props
if (prop.type === Boolean) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
value = true
}
}
// check default value // check default value
if (value === undefined) { if (value === undefined) {
value = getPropDefaultValue(vm, prop, key) value = getPropDefaultValue(vm, prop, key)
@ -37,10 +45,7 @@ export function validateProp (vm: Component, key: string, propsData: ?Object): a
function getPropDefaultValue (vm: Component, prop: PropOptions, name: string): any { function getPropDefaultValue (vm: Component, prop: PropOptions, name: string): any {
// no default, return undefined // no default, return undefined
if (!hasOwn(prop, 'default')) { if (!hasOwn(prop, 'default')) {
// absent boolean value defaults to false return undefined
return prop.type === Boolean
? false
: undefined
} }
const def = prop.default const def = prop.default
// warn against non-factory defaults for Object & Array // warn against non-factory defaults for Object & Array
@ -75,7 +80,7 @@ function assertProp (
) )
return return
} }
if (value == null) { if (value == null && !prop.required) {
return return
} }
let type = prop.type let type = prop.type
@ -94,7 +99,7 @@ function assertProp (
if (!valid) { if (!valid) {
warn( warn(
'Invalid prop: type check failed for prop "' + name + '".' + 'Invalid prop: type check failed for prop "' + name + '".' +
' Expected ' + expectedTypes.join(', ') + ' Expected ' + expectedTypes.map(capitalize).join(', ') +
', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.', ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.',
vm vm
) )

View File

@ -176,7 +176,7 @@ function extractProps (data: VNodeData, Ctor: Class<Component>): ?Object {
const attrs = data.attrs const attrs = data.attrs
const props = data.props const props = data.props
const staticAttrs = data.staticAttrs const staticAttrs = data.staticAttrs
if (!attrs && !props) { if (!attrs && !props && !staticAttrs) {
return res return res
} }
for (const key in propOptions) { for (const key in propOptions) {

View File

@ -0,0 +1,350 @@
import Vue from 'vue'
describe('Options props', () => {
it('array syntax', done => {
const vm = new Vue({
data: {
b: 'bar'
},
template: '<test v-bind:b="b" v-ref:child></test>',
components: {
test: {
props: ['b'],
template: '<div>{{b}}</div>'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('bar')
vm.b = 'baz'
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('baz')
vm.$refs.child.b = 'qux'
}).then(() => {
expect(vm.$el.innerHTML).toBe('qux')
}).then(done)
})
it('object syntax', done => {
const vm = new Vue({
data: {
b: 'bar'
},
template: '<test v-bind:b="b" v-ref:child></test>',
components: {
test: {
props: { b: String },
template: '<div>{{b}}</div>'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('bar')
vm.b = 'baz'
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('baz')
vm.$refs.child.b = 'qux'
}).then(() => {
expect(vm.$el.innerHTML).toBe('qux')
}).then(done)
})
it('warn mixed syntax', () => {
new Vue({
props: [{ b: String }]
})
expect('props must be strings when using array syntax').toHaveBeenWarned()
})
it('default values', () => {
const vm = new Vue({
data: {
b: undefined
},
template: '<test :b="b"></test>',
components: {
test: {
props: {
a: {
default: 'A' // absent
},
b: {
default: 'B' // undefined
}
},
template: '<div>{{a}}{{b}}</div>'
}
}
}).$mount()
expect(vm.$el.textContent).toBe('AB')
})
it('default value reactivity', done => {
const vm = new Vue({
props: {
a: {
default: () => ({ b: 1 })
}
},
propsData: {
a: undefined
},
template: '<div>{{ a.b }}</div>'
}).$mount()
expect(vm.$el.textContent).toBe('1')
vm.a.b = 2
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('2')
}).then(done)
})
it('warn object/array default values', () => {
new Vue({
props: {
a: {
default: { b: 1 }
}
},
propsData: {
a: undefined
}
})
expect('Props with type Object/Array must use a factory function').toHaveBeenWarned()
})
it('warn missing required', () => {
new Vue({
template: '<test></test>',
components: {
test: {
props: { a: { required: true }},
template: '<div>{{a}}</div>'
}
}
}).$mount()
expect('Missing required prop: "a"').toHaveBeenWarned()
})
describe('assertions', () => {
function makeInstance (value, type, validator, required) {
return new Vue({
template: '<test :test="val"></test>',
data: {
val: value
},
components: {
test: {
template: '<div></div>',
props: {
test: {
type,
validator,
required
}
}
}
}
}).$mount()
}
it('string', () => {
makeInstance('hello', String)
expect(console.error.calls.count()).toBe(0)
makeInstance(123, String)
expect('Expected String').toHaveBeenWarned()
})
it('number', () => {
makeInstance(123, Number)
expect(console.error.calls.count()).toBe(0)
makeInstance('123', Number)
expect('Expected Number').toHaveBeenWarned()
})
it('boolean', () => {
makeInstance(true, Boolean)
expect(console.error.calls.count()).toBe(0)
makeInstance('123', Boolean)
expect('Expected Boolean').toHaveBeenWarned()
})
it('function', () => {
makeInstance(() => {}, Function)
expect(console.error.calls.count()).toBe(0)
makeInstance(123, Function)
expect('Expected Function').toHaveBeenWarned()
})
it('object', () => {
makeInstance({}, Object)
expect(console.error.calls.count()).toBe(0)
makeInstance([], Object)
expect('Expected Object').toHaveBeenWarned()
})
it('array', () => {
makeInstance([], Array)
expect(console.error.calls.count()).toBe(0)
makeInstance({}, Array)
expect('Expected Array').toHaveBeenWarned()
})
it('custom constructor', () => {
function Class () {}
makeInstance(new Class(), Class)
expect(console.error.calls.count()).toBe(0)
makeInstance({}, Class)
expect('type check failed').toHaveBeenWarned()
})
it('multiple types', () => {
makeInstance([], [Array, Number, Boolean])
expect(console.error.calls.count()).toBe(0)
makeInstance({}, [Array, Number, Boolean])
expect('Expected Array, Number, Boolean, got Object').toHaveBeenWarned()
})
it('custom validator', () => {
makeInstance(123, null, v => v === 123)
expect(console.error.calls.count()).toBe(0)
makeInstance(123, null, v => v === 234)
expect('custom validator check failed').toHaveBeenWarned()
})
it('type check + custom validator', () => {
makeInstance(123, Number, v => v === 123)
expect(console.error.calls.count()).toBe(0)
makeInstance(123, Number, v => v === 234)
expect('custom validator check failed').toHaveBeenWarned()
makeInstance(123, String, v => v === 123)
expect('Expected String').toHaveBeenWarned()
})
it('multiple types + custom validator', () => {
makeInstance(123, [Number, String, Boolean], v => v === 123)
expect(console.error.calls.count()).toBe(0)
makeInstance(123, [Number, String, Boolean], v => v === 234)
expect('custom validator check failed').toHaveBeenWarned()
makeInstance(123, [String, Boolean], v => v === 123)
expect('Expected String, Boolean').toHaveBeenWarned()
})
it('optional with type + null/undefined', () => {
makeInstance(undefined, String)
expect(console.error.calls.count()).toBe(0)
makeInstance(null, String)
expect(console.error.calls.count()).toBe(0)
})
it('required with type + null/undefined', () => {
makeInstance(undefined, String, null, true)
expect(console.error.calls.count()).toBe(1)
expect('Expected String').toHaveBeenWarned()
makeInstance(null, Boolean, null, true)
expect(console.error.calls.count()).toBe(2)
expect('Expected Boolean').toHaveBeenWarned()
})
})
it('should warn data fields already defined as a prop', () => {
new Vue({
template: '<test a="1"></test>',
components: {
test: {
template: '<div></div>',
data: function () {
return { a: 123 }
},
props: {
a: null
}
}
}
}).$mount()
expect('already declared as a prop').toHaveBeenWarned()
})
it('treat boolean props properly', () => {
const vm = new Vue({
template: '<comp v-ref:child prop-a prop-b="prop-b"></comp>',
components: {
comp: {
template: '<div></div>',
props: {
propA: Boolean,
propB: Boolean,
propC: Boolean
}
}
}
}).$mount()
expect(vm.$refs.child.propA).toBe(true)
expect(vm.$refs.child.propB).toBe(true)
expect(vm.$refs.child.propC).toBe(false)
})
it('should respect default value of a Boolean prop', function () {
const vm = new Vue({
template: '<test></test>',
components: {
test: {
props: {
prop: {
type: Boolean,
default: true
}
},
template: '<div>{{prop}}</div>'
}
}
}).$mount()
expect(vm.$el.textContent).toBe('true')
})
it('non reactive values passed down as prop should not be converted', done => {
const a = Object.freeze({
nested: {
msg: 'hello'
}
})
const parent = new Vue({
template: '<comp :a="a.nested"></comp>',
data: {
a: a
},
components: {
comp: {
template: '<div></div>',
props: ['a']
}
}
}).$mount()
const child = parent.$children[0]
expect(child.a.msg).toBe('hello')
expect(child.a.__ob__).toBeUndefined() // should not be converted
parent.a = Object.freeze({
nested: {
msg: 'yo'
}
})
waitForUpdate(() => {
expect(child.a.msg).toBe('yo')
expect(child.a.__ob__).toBeUndefined()
}).then(done)
})
it('should not warn for non-required, absent prop', function () {
new Vue({
template: '<test></test>',
components: {
test: {
template: '<div></div>',
props: {
prop: {
type: String
}
}
}
}
}).$mount()
expect(console.error.calls.count()).toBe(0)
})
})