mirror of https://github.com/vuejs/vue.git
				
				
				
			
		
			
				
	
	
		
			583 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			583 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import Vue from 'vue'
 | |
| import { hasSymbol } from 'core/util/env'
 | |
| import testObjectOption from '../../../helpers/test-object-option'
 | |
| 
 | |
| describe('Options props', () => {
 | |
|   testObjectOption('props')
 | |
| 
 | |
|   it('array syntax', done => {
 | |
|     const vm = new Vue({
 | |
|       data: {
 | |
|         b: 'bar'
 | |
|       },
 | |
|       template: '<test v-bind:b="b" 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')
 | |
|       expect('Avoid mutating a prop directly').toHaveBeenWarned()
 | |
|     }).then(done)
 | |
|   })
 | |
| 
 | |
|   it('object syntax', done => {
 | |
|     const vm = new Vue({
 | |
|       data: {
 | |
|         b: 'bar'
 | |
|       },
 | |
|       template: '<test v-bind:b="b" 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')
 | |
|       expect('Avoid mutating a prop directly').toHaveBeenWarned()
 | |
|     }).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('default value Function', () => {
 | |
|     const func = () => 132
 | |
|     const vm = new Vue({
 | |
|       props: {
 | |
|         a: {
 | |
|           type: Function,
 | |
|           default: func
 | |
|         }
 | |
|       },
 | |
|       propsData: {
 | |
|         a: undefined
 | |
|       }
 | |
|     })
 | |
|     expect(vm.a).toBe(func)
 | |
|   })
 | |
| 
 | |
|   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.mock.calls.length).toBe(0)
 | |
|       makeInstance(123, String)
 | |
|       expect('Expected String with value "123", got Number with value 123').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('number', () => {
 | |
|       makeInstance(123, Number)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance('123', Number)
 | |
|       expect('Expected Number with value 123, got String with value "123"').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('number & boolean', () => {
 | |
|       makeInstance(123, Number)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(false, Number)
 | |
|       expect('Expected Number, got Boolean with value false').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('string & boolean', () => {
 | |
|       makeInstance('hello', String)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(true, String)
 | |
|       expect('Expected String, got Boolean with value true').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('boolean', () => {
 | |
|       makeInstance(true, Boolean)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance('123', Boolean)
 | |
|       expect('Expected Boolean, got String with value "123"').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('function', () => {
 | |
|       makeInstance(() => {}, Function)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(123, Function)
 | |
|       expect('Expected Function, got Number with value 123').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('object', () => {
 | |
|       makeInstance({}, Object)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance([], Object)
 | |
|       expect('Expected Object, got Array').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('array', () => {
 | |
|       makeInstance([], Array)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance({}, Array)
 | |
|       expect('Expected Array, got Object').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('primitive wrapper objects', () => {
 | |
|       /* eslint-disable no-new-wrappers */
 | |
|       makeInstance(new String('s'), String)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(new Number(1), Number)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(new Boolean(true), Boolean)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       /* eslint-enable no-new-wrappers */
 | |
|     })
 | |
| 
 | |
|     if (hasSymbol) {
 | |
|       it('symbol', () => {
 | |
|         makeInstance(Symbol('foo'), Symbol)
 | |
|         expect(console.error.mock.calls.length).toBe(0)
 | |
|         makeInstance({}, Symbol)
 | |
|         expect('Expected Symbol, got Object').toHaveBeenWarned()
 | |
|       })
 | |
| 
 | |
|       it('warns when expected an explicable type but Symbol was provided', () => {
 | |
|         makeInstance(Symbol('foo'), String)
 | |
|         expect('Expected String, got Symbol').toHaveBeenWarned()
 | |
|       })
 | |
| 
 | |
|       it('warns when expected an explicable type but Symbol was provided', () => {
 | |
|         makeInstance(Symbol('foo'), [String, Number])
 | |
|         expect('Expected String, Number, got Symbol').toHaveBeenWarned()
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     if (typeof BigInt !== 'undefined') {
 | |
|       /* global BigInt */
 | |
|       it('bigint', () => {
 | |
|         makeInstance(BigInt(100), BigInt)
 | |
|         expect(console.error.mock.calls.length).toBe(0)
 | |
|         makeInstance({}, BigInt)
 | |
|         expect('Expected BigInt, got Object').toHaveBeenWarned()
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     it('custom constructor', () => {
 | |
|       function Class () {}
 | |
|       makeInstance(new Class(), Class)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance({}, Class)
 | |
|       expect('type check failed').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('multiple types', () => {
 | |
|       makeInstance([], [Array, Number, Boolean])
 | |
|       expect(console.error.mock.calls.length).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.mock.calls.length).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.mock.calls.length).toBe(0)
 | |
|       makeInstance(123, Number, v => v === 234)
 | |
|       expect('custom validator check failed').toHaveBeenWarned()
 | |
|       makeInstance(123, String, v => v === 123)
 | |
|       expect('Expected String with value "123", got Number with value 123').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('multiple types + custom validator', () => {
 | |
|       makeInstance(123, [Number, String, Boolean], v => v === 123)
 | |
|       expect(console.error.mock.calls.length).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.mock.calls.length).toBe(0)
 | |
|       makeInstance(null, String)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|     })
 | |
| 
 | |
|     it('required with type + null/undefined', () => {
 | |
|       makeInstance(undefined, String, null, true)
 | |
|       expect(console.error.mock.calls.length).toBe(1)
 | |
|       expect('Expected String').toHaveBeenWarned()
 | |
|       makeInstance(null, Boolean, null, true)
 | |
|       expect(console.error.mock.calls.length).toBe(2)
 | |
|       expect('Expected Boolean').toHaveBeenWarned()
 | |
|     })
 | |
| 
 | |
|     it('optional prop of any type (type: true or prop: true)', () => {
 | |
|       makeInstance(1, true)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance('any', true)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance({}, true)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(undefined, true)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|       makeInstance(null, true)
 | |
|       expect(console.error.mock.calls.length).toBe(0)
 | |
|     })
 | |
|   })
 | |
| 
 | |
|   it('should work with v-bind', () => {
 | |
|     const vm = new Vue({
 | |
|       template: `<test v-bind="{ a: 1, b: 2 }"></test>`,
 | |
|       components: {
 | |
|         test: {
 | |
|           props: ['a', 'b'],
 | |
|           template: '<div>{{ a }} {{ b }}</div>'
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
|     expect(vm.$el.textContent).toBe('1 2')
 | |
|   })
 | |
| 
 | |
|   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('should warn methods already defined as a prop', () => {
 | |
|     new Vue({
 | |
|       template: '<test a="1"></test>',
 | |
|       components: {
 | |
|         test: {
 | |
|           template: '<div></div>',
 | |
|           props: {
 | |
|             a: null
 | |
|           },
 | |
|           methods: {
 | |
|             a () {
 | |
| 
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
|     expect(`Method "a" has already been defined as a prop`).toHaveBeenWarned()
 | |
|     expect(`Avoid mutating a prop directly`).toHaveBeenWarned()
 | |
|   })
 | |
| 
 | |
|   it('treat boolean props properly', () => {
 | |
|     const vm = new Vue({
 | |
|       template: '<comp 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.mock.calls.length).toBe(0)
 | |
|   })
 | |
| 
 | |
|   // #3453
 | |
|   it('should not fire watcher on object/array props when parent re-renders', done => {
 | |
|     const spy = vi.fn()
 | |
|     const vm = new Vue({
 | |
|       data: {
 | |
|         arr: []
 | |
|       },
 | |
|       template: '<test :prop="arr">hi</test>',
 | |
|       components: {
 | |
|         test: {
 | |
|           props: ['prop'],
 | |
|           watch: {
 | |
|             prop: spy
 | |
|           },
 | |
|           template: '<div><slot></slot></div>'
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
|     vm.$forceUpdate()
 | |
|     waitForUpdate(() => {
 | |
|       expect(spy).not.toHaveBeenCalled()
 | |
|     }).then(done)
 | |
|   })
 | |
| 
 | |
|   // #4090
 | |
|   it('should not trigger watcher on default value', done => {
 | |
|     const spy = vi.fn()
 | |
|     const vm = new Vue({
 | |
|       template: `<test :value="a" :test="b"></test>`,
 | |
|       data: {
 | |
|         a: 1,
 | |
|         b: undefined
 | |
|       },
 | |
|       components: {
 | |
|         test: {
 | |
|           template: '<div>{{ value }}</div>',
 | |
|           props: {
 | |
|             value: { type: Number },
 | |
|             test: {
 | |
|               type: Object,
 | |
|               default: () => ({})
 | |
|             }
 | |
|           },
 | |
|           watch: {
 | |
|             test: spy
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
| 
 | |
|     vm.a++
 | |
|     waitForUpdate(() => {
 | |
|       expect(spy).not.toHaveBeenCalled()
 | |
|       vm.b = {}
 | |
|     }).then(() => {
 | |
|       expect(spy.mock.calls.length).toBe(1)
 | |
|     }).then(() => {
 | |
|       vm.b = undefined
 | |
|     }).then(() => {
 | |
|       expect(spy.mock.calls.length).toBe(2)
 | |
|       vm.a++
 | |
|     }).then(() => {
 | |
|       expect(spy.mock.calls.length).toBe(2)
 | |
|     }).then(done)
 | |
|   })
 | |
| 
 | |
|   it('warn reserved props', () => {
 | |
|     const specialAttrs = ['key', 'ref', 'slot', 'is', 'slot-scope']
 | |
|     new Vue({
 | |
|       props: specialAttrs
 | |
|     })
 | |
|     specialAttrs.forEach(attr => {
 | |
|       expect(`"${attr}" is a reserved attribute`).toHaveBeenWarned()
 | |
|     })
 | |
|   })
 | |
| 
 | |
|   it('should consider order when casting [Boolean, String] multi-type props', () => {
 | |
|     const vm = new Vue({
 | |
|       template: '<test ref="test" booleanOrString stringOrBoolean />',
 | |
|       components: {
 | |
|         test: {
 | |
|           template: '<div></div>',
 | |
|           props: {
 | |
|             booleanOrString: [Boolean, String],
 | |
|             stringOrBoolean: [String, Boolean]
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
|     expect(vm.$refs.test.$props.booleanOrString).toBe(true)
 | |
|     expect(vm.$refs.test.$props.stringOrBoolean).toBe('')
 | |
|   })
 | |
| 
 | |
|   it('should warn when a prop type is not a constructor', () => {
 | |
|     const vm = new Vue({
 | |
|       template: '<div>{{a}}</div>',
 | |
|       props: {
 | |
|         a: {
 | |
|           type: 'String',
 | |
|           default: 'test'
 | |
|         }
 | |
|       }
 | |
|     }).$mount()
 | |
|     expect(
 | |
|       'Invalid prop type: "String" is not a constructor'
 | |
|     ).toHaveBeenWarned()
 | |
|   })
 | |
| 
 | |
| })
 |