From 01bbb7d84cd46dcc268de9ba0629c37400e1c602 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 23 Jun 2018 01:48:51 +0900 Subject: [PATCH 1/2] feat: element, vnode, vm, and options are read-only --- docs/api/wrapper/README.md | 8 +- packages/test-utils/src/vue-wrapper.js | 6 +- packages/test-utils/src/wrapper.js | 167 ++++++++++-------------- test/specs/vuewrapper.js | 13 ++ test/specs/wrapper.spec.js | 14 ++ test/specs/wrapper/hasAttribute.spec.js | 7 - test/specs/wrapper/setChecked.spec.js | 13 -- test/specs/wrapper/setSelected.spec.js | 13 -- test/specs/wrapper/setValue.spec.js | 11 -- test/specs/wrapper/text.spec.js | 11 -- test/specs/wrapper/trigger.spec.js | 12 -- 11 files changed, 103 insertions(+), 172 deletions(-) create mode 100644 test/specs/vuewrapper.js create mode 100644 test/specs/wrapper.spec.js diff --git a/docs/api/wrapper/README.md b/docs/api/wrapper/README.md index 78910e581..b4765b6f4 100644 --- a/docs/api/wrapper/README.md +++ b/docs/api/wrapper/README.md @@ -8,21 +8,21 @@ A `Wrapper` is an object that contains a mounted component or vnode and methods ### `vm` -`Component`: This is the `Vue` instance. You can access all the [instance methods and properties of a vm](https://vuejs.org/v2/api/#Instance-Properties) with `wrapper.vm`. This only exists on Vue component wrappers +`Component` (read-only): This is the `Vue` instance. You can access all the [instance methods and properties of a vm](https://vuejs.org/v2/api/#Instance-Properties) with `wrapper.vm`. This only exists on Vue component wrappers. ### `element` -`HTMLElement`: the root DOM node of the wrapper +`HTMLElement` (read-only): the root DOM node of the wrapper ### `options` #### `options.attachedToDocument` -`Boolean`: True if `attachedToDocument` in mounting options was true  +`Boolean` (read-only): True if `attachedToDocument` in mounting options was `true` #### `options.sync` -`Boolean`: True if `sync` in mounting options was not `false` +`Boolean` (read-only): True if `sync` in mounting options was not `false` ## Methods diff --git a/packages/test-utils/src/vue-wrapper.js b/packages/test-utils/src/vue-wrapper.js index 3f1820f76..289790a6b 100644 --- a/packages/test-utils/src/vue-wrapper.js +++ b/packages/test-utils/src/vue-wrapper.js @@ -18,7 +18,11 @@ export default class VueWrapper extends Wrapper implements BaseWrapper { get: () => vm.$el, set: () => {} }) - this.vm = vm + // $FlowIgnore + Object.defineProperty(this, 'vm', { + get: () => vm, + set: () => {} + }) if (options.sync) { setWatchersToSync(vm) orderWatchers(vm) diff --git a/packages/test-utils/src/wrapper.js b/packages/test-utils/src/wrapper.js index 371178cb5..1b29a4f0c 100644 --- a/packages/test-utils/src/wrapper.js +++ b/packages/test-utils/src/wrapper.js @@ -23,32 +23,50 @@ import createWrapper from './create-wrapper' import { orderWatchers } from './order-watchers' export default class Wrapper implements BaseWrapper { - vnode: VNode | null; - vm: Component | null; + +vnode: VNode | null; + +vm: Component | null; _emitted: { [name: string]: Array> }; _emittedByOrder: Array<{ name: string, args: Array }>; isVm: boolean; - element: Element; + +element: Element; update: Function; - options: WrapperOptions; + +options: WrapperOptions; version: number; isFunctionalComponent: boolean; constructor (node: VNode | Element, options: WrapperOptions) { - if (node instanceof Element) { - this.element = node - this.vnode = null - } else { - this.vnode = node - this.element = node.elm + const vnode = node instanceof Element ? null : node + const element = node instanceof Element ? node : node.elm + // Prevent redefine by VueWrapper + if (this.constructor.name === 'Wrapper') { + // $FlowIgnore + Object.defineProperty(this, 'vnode', { + get: () => vnode, + set: () => {} + }) + // $FlowIgnore + Object.defineProperty(this, 'element', { + get: () => element, + set: () => {} + }) + // $FlowIgnore + Object.defineProperty(this, 'vm', { + get: () => undefined, + set: () => {} + }) } + const freezedOptions = Object.freeze(options) + // $FlowIgnore + Object.defineProperty(this, 'options', { + get: () => freezedOptions, + set: () => {} + }) if ( this.vnode && (this.vnode[FUNCTIONAL_OPTIONS] || this.vnode.functionalContext) ) { this.isFunctionalComponent = true } - this.options = options this.version = Number( `${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}` ) @@ -112,7 +130,7 @@ export default class Wrapper implements BaseWrapper { */ emitted (event?: string) { if (!this._emitted && !this.vm) { - throwError(`wrapper.emitted() can only be called on a Vue ` + `instance`) + throwError(`wrapper.emitted() can only be called on a Vue instance`) } if (event) { return this._emitted[event] @@ -126,7 +144,7 @@ export default class Wrapper implements BaseWrapper { emittedByOrder () { if (!this._emittedByOrder && !this.vm) { throwError( - `wrapper.emittedByOrder() can only be called on a ` + `Vue instance` + `wrapper.emittedByOrder() can only be called on a Vue instance` ) } return this._emittedByOrder @@ -155,13 +173,7 @@ export default class Wrapper implements BaseWrapper { `visible has been deprecated and will be removed in ` + `version 1, use isVisible instead` ) - let element = this.element - - if (!element) { - return false - } - while (element) { if ( element.style && @@ -188,17 +200,17 @@ export default class Wrapper implements BaseWrapper { if (typeof attribute !== 'string') { throwError( - `wrapper.hasAttribute() must be passed attribute as ` + `a string` + `wrapper.hasAttribute() must be passed attribute as a string` ) } if (typeof value !== 'string') { throwError( - `wrapper.hasAttribute() must be passed value as a ` + `string` + `wrapper.hasAttribute() must be passed value as a string` ) } - return !!(this.element && this.element.getAttribute(attribute) === value) + return !!(this.element.getAttribute(attribute) === value) } /** @@ -270,7 +282,7 @@ export default class Wrapper implements BaseWrapper { ) if (typeof style !== 'string') { - throwError(`wrapper.hasStyle() must be passed style as a ` + `string`) + throwError(`wrapper.hasStyle() must be passed style as a string`) } if (typeof value !== 'string') { @@ -413,11 +425,6 @@ export default class Wrapper implements BaseWrapper { */ isVisible (): boolean { let element = this.element - - if (!element) { - return false - } - while (element) { if ( element.style && @@ -669,41 +676,32 @@ export default class Wrapper implements BaseWrapper { * Sets element value and triggers input event */ setValue (value: any) { - const el = this.element - - if (!el) { - throwError( - `cannot call wrapper.setValue() on a wrapper ` + `without an element` - ) - } - - const tag = el.tagName + const tagName = this.element.tagName const type = this.attributes().type - const event = 'input' - if (tag === 'SELECT') { + if (tagName === 'SELECT') { throwError( `wrapper.setValue() cannot be called on a element. Use ` + `wrapper.setChecked() instead` ) - } else if (tag === 'INPUT' && type === 'radio') { + } else if (tagName === 'INPUT' && type === 'radio') { throwError( `wrapper.setValue() cannot be called on a element. Use wrapper.setChecked() ` + `instead` ) - } else if (tag === 'INPUT' || tag === 'textarea') { + } else if (tagName === 'INPUT' || tagName === 'textarea') { // $FlowIgnore - el.value = value - this.trigger(event) + this.element.value = value + this.trigger('input') } else { - throwError(`wrapper.setValue() cannot be called on this ` + `element`) + throwError(`wrapper.setValue() cannot be called on this element`) } } @@ -714,36 +712,26 @@ export default class Wrapper implements BaseWrapper { if (typeof checked !== 'boolean') { throwError('wrapper.setChecked() must be passed a boolean') } - - const el = this.element - - if (!el) { - throwError( - `cannot call wrapper.setChecked() on a wrapper ` + `without an element` - ) - } - - const tag = el.tagName + const tagName = this.element.tagName const type = this.attributes().type - const event = 'change' - if (tag === 'SELECT') { + if (tagName === 'SELECT') { throwError( `wrapper.setChecked() cannot be called on a ` + ` element. Use ` + `wrapper.setChecked() instead` ) - } else if (tag === 'INPUT' && type === 'radio') { + } else if (tagName === 'INPUT' && type === 'radio') { throwError( `wrapper.setSelected() cannot be called on a element. Use wrapper.setChecked() ` + `instead` ) - } else if (tag === 'INPUT' || tag === 'textarea') { + } else if (tagName === 'INPUT' || tagName === 'textarea') { throwError( `wrapper.setSelected() cannot be called on "text" ` + `inputs. Use wrapper.setValue() instead` ) } else { - throwError(`wrapper.setSelected() cannot be called on this ` + `element`) + throwError(`wrapper.setSelected() cannot be called on this element`) } } @@ -827,12 +806,6 @@ export default class Wrapper implements BaseWrapper { * Return text of wrapper element */ text (): string { - if (!this.element) { - throwError( - `cannot call wrapper.text() on a wrapper without an ` + `element` - ) - } - return this.element.textContent.trim() } @@ -859,12 +832,6 @@ export default class Wrapper implements BaseWrapper { throwError('wrapper.trigger() must be passed a string') } - if (!this.element) { - throwError( - `cannot call wrapper.trigger() on a wrapper without ` + `an element` - ) - } - if (options.target) { throwError( `you cannot set the target value of an event. See ` + diff --git a/test/specs/vuewrapper.js b/test/specs/vuewrapper.js new file mode 100644 index 000000000..b909d9e31 --- /dev/null +++ b/test/specs/vuewrapper.js @@ -0,0 +1,13 @@ +import { describeWithShallowAndMount } from '~resources/utils' + +describeWithShallowAndMount('VueWrapper', mountingMethod => { + ['vnode', 'element', 'vm', 'options'].forEach(property => { + it(`has the ${property} property which is read-only`, () => { + const wrapper = mountingMethod({ template: '

' }) + expect(wrapper.constructor.name).to.equal('VueWrapper') + const originalProperty = wrapper[property] + wrapper[property] = 'foo' + expect(wrapper[property]).to.equal(originalProperty) + }) + }) +}) diff --git a/test/specs/wrapper.spec.js b/test/specs/wrapper.spec.js new file mode 100644 index 000000000..38fec40f0 --- /dev/null +++ b/test/specs/wrapper.spec.js @@ -0,0 +1,14 @@ +import { describeWithShallowAndMount } from '~resources/utils' + +describeWithShallowAndMount('Wrapper', mountingMethod => { + ['vnode', 'element', 'vm', 'options'].forEach(property => { + it(`has the ${property} property which is read-only`, () => { + const wrapper = mountingMethod({ template: '

' }) + .find('p') + expect(wrapper.constructor.name).to.equal('Wrapper') + const originalProperty = wrapper[property] + wrapper[property] = 'foo' + expect(wrapper[property]).to.equal(originalProperty) + }) + }) +}) diff --git a/test/specs/wrapper/hasAttribute.spec.js b/test/specs/wrapper/hasAttribute.spec.js index 27cdad613..416a7780b 100644 --- a/test/specs/wrapper/hasAttribute.spec.js +++ b/test/specs/wrapper/hasAttribute.spec.js @@ -16,13 +16,6 @@ describeWithShallowAndMount('hasAttribute', mountingMethod => { expect(wrapper.hasAttribute('attribute', 'value')).to.equal(false) }) - it('returns false if wrapper element is null', () => { - const compiled = compileToFunctions('
') - const wrapper = mountingMethod(compiled) - wrapper.element = null - expect(wrapper.hasAttribute('attribute', 'value')).to.equal(false) - }) - it('throws an error if attribute is not a string', () => { const compiled = compileToFunctions('
') const wrapper = mountingMethod(compiled) diff --git a/test/specs/wrapper/setChecked.spec.js b/test/specs/wrapper/setChecked.spec.js index 44e3423f7..805b504b8 100644 --- a/test/specs/wrapper/setChecked.spec.js +++ b/test/specs/wrapper/setChecked.spec.js @@ -83,19 +83,6 @@ describeWithShallowAndMount('setChecked', mountingMethod => { shouldThrowErrorOnElement('#radioFoo', message, false) }) - it('throws error if wrapper does not contain element', () => { - const wrapper = mountingMethod({ template: '

' }) - const p = wrapper.find('p') - p.element = null - - const fn = () => p.setChecked() - const message = - '[vue-test-utils]: cannot call wrapper.setChecked() on a wrapper without an element' - expect(fn) - .to.throw() - .with.property('message', message) - }) - it('throws error if element is select', () => { const message = 'wrapper.setChecked() cannot be called on a element. Use wrapper.setChecked() instead' diff --git a/test/specs/wrapper/setValue.spec.js b/test/specs/wrapper/setValue.spec.js index 0727a798a..e37566010 100644 --- a/test/specs/wrapper/setValue.spec.js +++ b/test/specs/wrapper/setValue.spec.js @@ -19,17 +19,6 @@ describeWithShallowAndMount('setValue', mountingMethod => { expect(wrapper.text()).to.contain('input text awesome binding') }) - it('throws error if wrapper does not contain element', () => { - const wrapper = mountingMethod({ template: '

' }) - const p = wrapper.find('p') - p.element = null - const fn = () => p.setValue('') - const message = '[vue-test-utils]: cannot call wrapper.setValue() on a wrapper without an element' - expect(fn) - .to.throw() - .with.property('message', message) - }) - it('throws error if element is select', () => { const message = 'wrapper.setValue() cannot be called on a