diff --git a/packages/test-utils/src/wrapper.js b/packages/test-utils/src/wrapper.js index b8beeca16..ea31cf192 100644 --- a/packages/test-utils/src/wrapper.js +++ b/packages/test-utils/src/wrapper.js @@ -478,63 +478,67 @@ export default class Wrapper implements BaseWrapper { * Sets vm props */ setProps(data: Object): void { - const originalConfig = Vue.config.silent - Vue.config.silent = config.silent + // Validate the setProps method call if (this.isFunctionalComponent) { throwError( `wrapper.setProps() cannot be called on a functional component` ) } + if (!this.vm) { throwError(`wrapper.setProps() can only be called on a Vue instance`) } - Object.keys(data).forEach(key => { - if ( - typeof data[key] === 'object' && - data[key] !== null && - // $FlowIgnore : Problem with possibly null this.vm - data[key] === this.vm[key] - ) { - throwError( - `wrapper.setProps() called with the same object of the existing ` + - `${key} property. You must call wrapper.setProps() with a new ` + - `object to trigger reactivity` - ) - } - if ( - !this.vm || - !this.vm.$options._propKeys || - !this.vm.$options._propKeys.some(prop => prop === key) - ) { - if (VUE_VERSION > 2.3) { + // Save the original "silent" config so that we can directly mutate props + const originalConfig = Vue.config.silent + Vue.config.silent = config.silent + + try { + Object.keys(data).forEach(key => { + // Don't let people set entire objects, because reactivity won't work + if ( + typeof data[key] === 'object' && + data[key] !== null && // $FlowIgnore : Problem with possibly null this.vm - this.vm.$attrs[key] = data[key] - return + data[key] === this.vm[key] + ) { + throwError( + `wrapper.setProps() called with the same object of the existing ` + + `${key} property. You must call wrapper.setProps() with a new ` + + `object to trigger reactivity` + ) } - throwError( - `wrapper.setProps() called with ${key} property which ` + - `is not defined on the component` - ) - } - if (this.vm && this.vm._props) { - // Set actual props value - this.vm._props[key] = data[key] - // $FlowIgnore : Problem with possibly null this.vm - this.vm[key] = data[key] - } else { - // $FlowIgnore : Problem with possibly null this.vm.$options - this.vm.$options.propsData[key] = data[key] + if ( + !this.vm || + !this.vm.$options._propKeys || + !this.vm.$options._propKeys.some(prop => prop === key) + ) { + if (VUE_VERSION > 2.3) { + // $FlowIgnore : Problem with possibly null this.vm + this.vm.$attrs[key] = data[key] + return + } + throwError( + `wrapper.setProps() called with ${key} property which ` + + `is not defined on the component` + ) + } + + // Actually set the prop // $FlowIgnore : Problem with possibly null this.vm this.vm[key] = data[key] - // $FlowIgnore : Need to call this twice to fix watcher bug in 2.0.x - this.vm[key] = data[key] - } - }) - // $FlowIgnore : Problem with possibly null this.vm - this.vm.$forceUpdate() - Vue.config.silent = originalConfig + }) + + // $FlowIgnore : Problem with possibly null this.vm + this.vm.$forceUpdate() + } catch (err) { + throw err + } finally { + // Ensure you teardown the modifications you made to the user's config + // After all the props are set, then reset the state + Vue.config.silent = originalConfig + } } /** diff --git a/test/specs/wrapper/setProps.spec.js b/test/specs/wrapper/setProps.spec.js index de74807dd..52f0bd23a 100644 --- a/test/specs/wrapper/setProps.spec.js +++ b/test/specs/wrapper/setProps.spec.js @@ -42,66 +42,15 @@ describeWithShallowAndMount('setProps', mountingMethod => { expect(wrapper.is('div')).to.equal(true) }) - itDoNotRunIf( - vueVersion > 2.3, - 'throws error if component does not include props key', - () => { - const TestComponent = { - template: '
' - } - const message = - `[vue-test-utils]: wrapper.setProps() called ` + - `with prop1 property which is not defined on the component` - const fn = () => mountingMethod(TestComponent).setProps({ prop1: 'prop' }) - expect(fn) - .to.throw() - .with.property('message', message) - } - ) - - itDoNotRunIf( - vueVersion < 2.4, - 'attributes not recognized as props are available via the $attrs instance property', - () => { - const TestComponent = { - template: '
' - } - const prop1 = 'prop1' - const wrapper = mountingMethod(TestComponent) - wrapper.setProps({ prop1 }) - expect(wrapper.vm.$attrs.prop1).to.equal(prop1) - } - ) - - it('throws error when called on functional vnode', () => { - const AFunctionalComponent = { - render: (h, context) => h('div', context.prop1), - functional: true - } - const message = - '[vue-test-utils]: wrapper.setProps() cannot be called on a functional component' - const fn = () => - mountingMethod(AFunctionalComponent).setProps({ prop1: 'prop' }) - expect(fn) - .to.throw() - .with.property('message', message) - // find on functional components isn't supported in Vue < 2.3 - if (vueVersion < 2.3) { - return - } + it('setProps and props getter are in sync', () => { const TestComponent = { - template: '
', - components: { - AFunctionalComponent - } + template: `
`, + props: { prop1: { default: 'initial value' } } } - const fn2 = () => - mountingMethod(TestComponent) - .find(AFunctionalComponent) - .setProps({ prop1: 'prop' }) - expect(fn2) - .to.throw() - .with.property('message', message) + const wrapper = mountingMethod(TestComponent) + const updatedValue = 'updated value' + wrapper.setProps({ prop1: updatedValue }) + expect(wrapper.props().prop1).to.equal(updatedValue) }) it('sets component props, and updates DOM when propsData was not initially passed', async () => { @@ -114,83 +63,124 @@ describeWithShallowAndMount('setProps', mountingMethod => { expect(wrapper.find('.prop-2').element.textContent).to.equal(prop2) }) - it('runs watch function when prop is updated', async () => { - const wrapper = mountingMethod(ComponentWithWatch) - const prop1 = 'testest' - wrapper.setProps({ prop1 }) - await Vue.nextTick() - expect(wrapper.vm.prop2).to.equal(prop1) + describe('attrs', () => { + itDoNotRunIf( + vueVersion < 2.4, + 'attributes not recognized as props are available via the $attrs instance property', + () => { + const TestComponent = { + template: '
' + } + const prop1 = 'prop1' + const wrapper = mountingMethod(TestComponent) + wrapper.setProps({ prop1 }) + expect(wrapper.vm.$attrs.prop1).to.equal(prop1) + } + ) }) - it('should not run watchers if prop updated is null', async () => { - const TestComponent = { - template: ` -
-
There is no message yet
-
{{ reversedMessage }}
-
- `, - computed: { - reversedMessage: function() { - return this.message - .split('') - .reverse() - .join('') + describe('watchers', () => { + it('updates watched prop', () => { + const TestComponent = { + template: '
', + props: ['propA'], + mounted() { + this.$watch( + 'propA', + function() { + this.propA + }, + { immediate: true } + ) } - }, - props: ['message'] - } - const wrapper = mountingMethod(TestComponent, { - propsData: { - message: 'message' } + const wrapper = mountingMethod(TestComponent, { + propsData: { propA: 'none' } + }) + + wrapper.setProps({ propA: 'value' }) + expect(wrapper.props().propA).to.equal('value') + expect(wrapper.vm.propA).to.equal('value') }) - wrapper.setProps({ message: null }) - await Vue.nextTick() - expect(wrapper.text()).to.equal('There is no message yet') - }) - it('runs watchers correctly', async () => { - const TestComponent = { - template: `
- {{ stringified }} -
`, - props: ['collection'], - data: () => ({ - data: '' - }), - computed: { - stringified() { - return this.collection.join(',') - } - }, - watch: { - collection: 'execute' - }, - methods: { - execute() { - this.data = this.stringified + it('runs watchers correctly', async () => { + const TestComponent = { + template: `
+ {{ stringified }} +
`, + props: ['collection'], + data: () => ({ + data: '' + }), + computed: { + stringified() { + return this.collection.join(',') + } + }, + watch: { + collection: 'execute' + }, + methods: { + execute() { + this.data = this.stringified + } } } - } - const wrapper = mountingMethod(TestComponent, { - propsData: { collection: [] } + const wrapper = mountingMethod(TestComponent, { + propsData: { collection: [] } + }) + expect(wrapper.vm.stringified).to.equal('') + expect(wrapper.vm.data).to.equal('') + + wrapper.setProps({ collection: [1, 2, 3] }) + await Vue.nextTick() + expect(wrapper.vm.stringified).to.equal('1,2,3') + expect(wrapper.vm.data).to.equal('1,2,3') + + wrapper.vm.collection.push(4, 5) + await Vue.nextTick() + expect(wrapper.vm.stringified).to.equal('1,2,3,4,5') + expect(wrapper.vm.data).to.equal('1,2,3,4,5') }) - expect(wrapper.vm.stringified).to.equal('') - expect(wrapper.vm.data).to.equal('') - wrapper.setProps({ collection: [1, 2, 3] }) - await Vue.nextTick() - expect(wrapper.vm.stringified).to.equal('1,2,3') - expect(wrapper.vm.data).to.equal('1,2,3') + it('should not run watchers if prop updated is null', async () => { + const TestComponent = { + template: ` +
+
There is no message yet
+
{{ reversedMessage }}
+
+ `, + computed: { + reversedMessage: function() { + return this.message + .split('') + .reverse() + .join('') + } + }, + props: ['message'] + } + const wrapper = mountingMethod(TestComponent, { + propsData: { + message: 'message' + } + }) + wrapper.setProps({ message: null }) + await Vue.nextTick() + expect(wrapper.text()).to.equal('There is no message yet') + }) - wrapper.vm.collection.push(4, 5) - await Vue.nextTick() - expect(wrapper.vm.stringified).to.equal('1,2,3,4,5') - expect(wrapper.vm.data).to.equal('1,2,3,4,5') + it('runs watch function when prop is updated', async () => { + const wrapper = mountingMethod(ComponentWithWatch) + const prop1 = 'testest' + wrapper.setProps({ prop1 }) + await Vue.nextTick() + expect(wrapper.vm.prop2).to.equal(prop1) + }) }) - it('should same reference when called with same object', () => { + it('props and setProps should return the same reference when called with same object', () => { const TestComponent = { template: `
`, props: ['obj'] @@ -201,54 +191,82 @@ describeWithShallowAndMount('setProps', mountingMethod => { expect(wrapper.props().obj).to.equal(obj) }) - it('throws an error if property is same reference', () => { - const TestComponent = { - template: `
`, - props: ['obj'] + describe('invalid arguments', () => { + const errors = { + FUNCTIONAL_COMPONENT_ERROR: `[vue-test-utils]: wrapper.setProps() cannot be called on a functional component`, + SAME_REFERENCE_ERROR: `[vue-test-utils]: wrapper.setProps() called with the same object of the existing obj property. You must call wrapper.setProps() with a new object to trigger reactivity`, + INVALID_NODE_ERROR: `wrapper.setProps() can only be called on a Vue instance`, + WRONG_PROP_ERROR: `[vue-test-utils]: wrapper.setProps() called with prop1 property which is not defined on the component` } - const obj = {} - const wrapper = mountingMethod(TestComponent, { - propsData: { - obj + + describe('functional components', () => { + const AFunctionalComponent = { + render: (h, context) => h('div', context.prop1), + functional: true } - }) - const message = - '[vue-test-utils]: wrapper.setProps() called with the same object of the existing obj property. You must call wrapper.setProps() with a new object to trigger reactivity' - const fn = () => wrapper.setProps({ obj }) - expect(fn) - .to.throw() - .with.property('message', message) - }) + it('throws error when called on functional vnode', () => { + const fn = () => + mountingMethod(AFunctionalComponent).setProps({ prop1: 'prop' }) + expect(fn).throw(Error, errors.FUNCTIONAL_COMPONENT_ERROR) + }) - it('updates watched prop', () => { - const TestComponent = { - template: '
', - props: ['propA'], - mounted() { - this.$watch( - 'propA', - function() { - this.propA - }, - { immediate: true } - ) - } - } - const wrapper = mountingMethod(TestComponent, { - propsData: { propA: 'none' } + // find on functional components isn't supported in Vue < 2.3 + itDoNotRunIf( + vueVersion < 2.3, + 'throws error after finding a functional component', + () => { + const TestComponent = { + template: '
', + components: { + AFunctionalComponent + } + } + const fn2 = () => + mountingMethod(TestComponent) + .find(AFunctionalComponent) + .setProps({ prop1: 'prop' }) + expect(fn2) + .to.throw() + .with.property('message', errors.FUNCTIONAL_COMPONENT_ERROR) + } + ) + + itDoNotRunIf( + vueVersion > 2.3, + 'throws error if component does not include props key', + () => { + const fn = () => + mountingMethod({ template: '
' }).setProps({ prop1: 'prop' }) + + expect(fn).throw(Error, errors.WRONG_PROP_ERROR) + } + ) }) - wrapper.setProps({ propA: 'value' }) - expect(wrapper.props().propA).to.equal('value') - expect(wrapper.vm.propA).to.equal('value') - }) + it('throws an error if property is same reference', () => { + const obj = {} + const wrapper = mountingMethod( + { + template: ` +
`, + props: ['obj'] + }, + { propsData: { obj } } + ) - it('throws an error if node is not a Vue instance', () => { - const message = 'wrapper.setProps() can only be called on a Vue instance' - const compiled = compileToFunctions('

') - const wrapper = mountingMethod(compiled) - const p = wrapper.find('p') - expect(() => p.setProps({ ready: true })).throw(Error, message) + const fn = () => wrapper.setProps({ obj }) + expect(fn).throw(Error, errors.SAME_REFERENCE_ERROR) + }) + + it('throws an error if node is not a Vue instance', () => { + const compiled = compileToFunctions('

') + const wrapper = mountingMethod(compiled) + const p = wrapper.find('p') + expect(() => p.setProps({ ready: true })).throw( + Error, + errors.INVALID_NODE_ERROR + ) + }) }) })