Skip to content

Commit b801c25

Browse files
38elementseddyerburgh
authored andcommitted
feat: element, vnode, vm, and options are read-only (#748)
breaking change
1 parent fccb3cb commit b801c25

File tree

11 files changed

+103
-172
lines changed

11 files changed

+103
-172
lines changed

Diff for: docs/api/wrapper/README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ A `Wrapper` is an object that contains a mounted component or vnode and methods
88

99
### `vm`
1010

11-
`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
11+
`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.
1212

1313
### `element`
1414

15-
`HTMLElement`: the root DOM node of the wrapper
15+
`HTMLElement` (read-only): the root DOM node of the wrapper
1616

1717
### `options`
1818

1919
#### `options.attachedToDocument`
2020

21-
`Boolean`: True if `attachedToDocument` in mounting options was true 
21+
`Boolean` (read-only): True if `attachedToDocument` in mounting options was `true`
2222

2323
#### `options.sync`
2424

25-
`Boolean`: True if `sync` in mounting options was not `false`
25+
`Boolean` (read-only): True if `sync` in mounting options was not `false`
2626

2727
## Methods
2828

Diff for: packages/test-utils/src/vue-wrapper.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ export default class VueWrapper extends Wrapper implements BaseWrapper {
1818
get: () => vm.$el,
1919
set: () => {}
2020
})
21-
this.vm = vm
21+
// $FlowIgnore
22+
Object.defineProperty(this, 'vm', {
23+
get: () => vm,
24+
set: () => {}
25+
})
2226
if (options.sync) {
2327
setWatchersToSync(vm)
2428
orderWatchers(vm)

Diff for: packages/test-utils/src/wrapper.js

+67-100
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,50 @@ import createWrapper from './create-wrapper'
2323
import { orderWatchers } from './order-watchers'
2424

2525
export default class Wrapper implements BaseWrapper {
26-
vnode: VNode | null;
27-
vm: Component | null;
26+
+vnode: VNode | null;
27+
+vm: Component | null;
2828
_emitted: { [name: string]: Array<Array<any>> };
2929
_emittedByOrder: Array<{ name: string, args: Array<any> }>;
3030
isVm: boolean;
31-
element: Element;
31+
+element: Element;
3232
update: Function;
33-
options: WrapperOptions;
33+
+options: WrapperOptions;
3434
version: number;
3535
isFunctionalComponent: boolean;
3636

3737
constructor (node: VNode | Element, options: WrapperOptions) {
38-
if (node instanceof Element) {
39-
this.element = node
40-
this.vnode = null
41-
} else {
42-
this.vnode = node
43-
this.element = node.elm
38+
const vnode = node instanceof Element ? null : node
39+
const element = node instanceof Element ? node : node.elm
40+
// Prevent redefine by VueWrapper
41+
if (this.constructor.name === 'Wrapper') {
42+
// $FlowIgnore
43+
Object.defineProperty(this, 'vnode', {
44+
get: () => vnode,
45+
set: () => {}
46+
})
47+
// $FlowIgnore
48+
Object.defineProperty(this, 'element', {
49+
get: () => element,
50+
set: () => {}
51+
})
52+
// $FlowIgnore
53+
Object.defineProperty(this, 'vm', {
54+
get: () => undefined,
55+
set: () => {}
56+
})
4457
}
58+
const frozenOptions = Object.freeze(options)
59+
// $FlowIgnore
60+
Object.defineProperty(this, 'options', {
61+
get: () => frozenOptions,
62+
set: () => {}
63+
})
4564
if (
4665
this.vnode &&
4766
(this.vnode[FUNCTIONAL_OPTIONS] || this.vnode.functionalContext)
4867
) {
4968
this.isFunctionalComponent = true
5069
}
51-
this.options = options
5270
this.version = Number(
5371
`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`
5472
)
@@ -112,7 +130,7 @@ export default class Wrapper implements BaseWrapper {
112130
*/
113131
emitted (event?: string) {
114132
if (!this._emitted && !this.vm) {
115-
throwError(`wrapper.emitted() can only be called on a Vue ` + `instance`)
133+
throwError(`wrapper.emitted() can only be called on a Vue instance`)
116134
}
117135
if (event) {
118136
return this._emitted[event]
@@ -126,7 +144,7 @@ export default class Wrapper implements BaseWrapper {
126144
emittedByOrder () {
127145
if (!this._emittedByOrder && !this.vm) {
128146
throwError(
129-
`wrapper.emittedByOrder() can only be called on a ` + `Vue instance`
147+
`wrapper.emittedByOrder() can only be called on a Vue instance`
130148
)
131149
}
132150
return this._emittedByOrder
@@ -155,13 +173,7 @@ export default class Wrapper implements BaseWrapper {
155173
`visible has been deprecated and will be removed in ` +
156174
`version 1, use isVisible instead`
157175
)
158-
159176
let element = this.element
160-
161-
if (!element) {
162-
return false
163-
}
164-
165177
while (element) {
166178
if (
167179
element.style &&
@@ -188,17 +200,17 @@ export default class Wrapper implements BaseWrapper {
188200

189201
if (typeof attribute !== 'string') {
190202
throwError(
191-
`wrapper.hasAttribute() must be passed attribute as ` + `a string`
203+
`wrapper.hasAttribute() must be passed attribute as a string`
192204
)
193205
}
194206

195207
if (typeof value !== 'string') {
196208
throwError(
197-
`wrapper.hasAttribute() must be passed value as a ` + `string`
209+
`wrapper.hasAttribute() must be passed value as a string`
198210
)
199211
}
200212

201-
return !!(this.element && this.element.getAttribute(attribute) === value)
213+
return !!(this.element.getAttribute(attribute) === value)
202214
}
203215

204216
/**
@@ -270,7 +282,7 @@ export default class Wrapper implements BaseWrapper {
270282
)
271283

272284
if (typeof style !== 'string') {
273-
throwError(`wrapper.hasStyle() must be passed style as a ` + `string`)
285+
throwError(`wrapper.hasStyle() must be passed style as a string`)
274286
}
275287

276288
if (typeof value !== 'string') {
@@ -413,11 +425,6 @@ export default class Wrapper implements BaseWrapper {
413425
*/
414426
isVisible (): boolean {
415427
let element = this.element
416-
417-
if (!element) {
418-
return false
419-
}
420-
421428
while (element) {
422429
if (
423430
element.style &&
@@ -669,41 +676,32 @@ export default class Wrapper implements BaseWrapper {
669676
* Sets element value and triggers input event
670677
*/
671678
setValue (value: any) {
672-
const el = this.element
673-
674-
if (!el) {
675-
throwError(
676-
`cannot call wrapper.setValue() on a wrapper ` + `without an element`
677-
)
678-
}
679-
680-
const tag = el.tagName
679+
const tagName = this.element.tagName
681680
const type = this.attributes().type
682-
const event = 'input'
683681

684-
if (tag === 'SELECT') {
682+
if (tagName === 'SELECT') {
685683
throwError(
686684
`wrapper.setValue() cannot be called on a <select> ` +
687685
`element. Use wrapper.setSelected() instead`
688686
)
689-
} else if (tag === 'INPUT' && type === 'checkbox') {
687+
} else if (tagName === 'INPUT' && type === 'checkbox') {
690688
throwError(
691689
`wrapper.setValue() cannot be called on a <input ` +
692690
`type="checkbox" /> element. Use ` +
693691
`wrapper.setChecked() instead`
694692
)
695-
} else if (tag === 'INPUT' && type === 'radio') {
693+
} else if (tagName === 'INPUT' && type === 'radio') {
696694
throwError(
697695
`wrapper.setValue() cannot be called on a <input ` +
698696
`type="radio" /> element. Use wrapper.setChecked() ` +
699697
`instead`
700698
)
701-
} else if (tag === 'INPUT' || tag === 'textarea') {
699+
} else if (tagName === 'INPUT' || tagName === 'textarea') {
702700
// $FlowIgnore
703-
el.value = value
704-
this.trigger(event)
701+
this.element.value = value
702+
this.trigger('input')
705703
} else {
706-
throwError(`wrapper.setValue() cannot be called on this ` + `element`)
704+
throwError(`wrapper.setValue() cannot be called on this element`)
707705
}
708706
}
709707

@@ -714,36 +712,26 @@ export default class Wrapper implements BaseWrapper {
714712
if (typeof checked !== 'boolean') {
715713
throwError('wrapper.setChecked() must be passed a boolean')
716714
}
717-
718-
const el = this.element
719-
720-
if (!el) {
721-
throwError(
722-
`cannot call wrapper.setChecked() on a wrapper ` + `without an element`
723-
)
724-
}
725-
726-
const tag = el.tagName
715+
const tagName = this.element.tagName
727716
const type = this.attributes().type
728-
const event = 'change'
729717

730-
if (tag === 'SELECT') {
718+
if (tagName === 'SELECT') {
731719
throwError(
732720
`wrapper.setChecked() cannot be called on a ` +
733721
`<select> element. Use wrapper.setSelected() ` +
734722
`instead`
735723
)
736-
} else if (tag === 'INPUT' && type === 'checkbox') {
724+
} else if (tagName === 'INPUT' && type === 'checkbox') {
737725
// $FlowIgnore
738-
if (el.checked !== checked) {
726+
if (this.element.checked !== checked) {
739727
if (!navigator.userAgent.includes('jsdom')) {
740728
// $FlowIgnore
741-
el.checked = checked
729+
this.element.checked = checked
742730
}
743731
this.trigger('click')
744-
this.trigger(event)
732+
this.trigger('change')
745733
}
746-
} else if (tag === 'INPUT' && type === 'radio') {
734+
} else if (tagName === 'INPUT' && type === 'radio') {
747735
if (!checked) {
748736
throwError(
749737
`wrapper.setChecked() cannot be called with ` +
@@ -752,87 +740,72 @@ export default class Wrapper implements BaseWrapper {
752740
)
753741
} else {
754742
// $FlowIgnore
755-
if (!el.checked) {
743+
if (!this.element.checked) {
756744
this.trigger('click')
757-
this.trigger(event)
745+
this.trigger('change')
758746
}
759747
}
760-
} else if (tag === 'INPUT' || tag === 'textarea') {
748+
} else if (tagName === 'INPUT' || tagName === 'textarea') {
761749
throwError(
762750
`wrapper.setChecked() cannot be called on "text" ` +
763751
`inputs. Use wrapper.setValue() instead`
764752
)
765753
} else {
766-
throwError(`wrapper.setChecked() cannot be called on this ` + `element`)
754+
throwError(`wrapper.setChecked() cannot be called on this element`)
767755
}
768756
}
769757

770758
/**
771759
* Selects <option></option> element
772760
*/
773761
setSelected () {
774-
const el = this.element
775-
776-
if (!el) {
777-
throwError(
778-
`cannot call wrapper.setSelected() on a wrapper ` + `without an element`
779-
)
780-
}
781-
782-
const tag = el.tagName
762+
const tagName = this.element.tagName
783763
const type = this.attributes().type
784-
const event = 'change'
785764

786-
if (tag === 'OPTION') {
765+
if (tagName === 'OPTION') {
787766
// $FlowIgnore
788-
el.selected = true
767+
this.element.selected = true
789768
// $FlowIgnore
790-
if (el.parentElement.tagName === 'OPTGROUP') {
769+
if (this.element.parentElement.tagName === 'OPTGROUP') {
791770
// $FlowIgnore
792-
createWrapper(el.parentElement.parentElement, this.options).trigger(
793-
event
794-
)
771+
createWrapper(this.element.parentElement.parentElement, this.options)
772+
.trigger('change')
795773
} else {
796774
// $FlowIgnore
797-
createWrapper(el.parentElement, this.options).trigger(event)
775+
createWrapper(this.element.parentElement, this.options)
776+
.trigger('change')
798777
}
799-
} else if (tag === 'SELECT') {
778+
} else if (tagName === 'SELECT') {
800779
throwError(
801780
`wrapper.setSelected() cannot be called on select. ` +
802781
`Call it on one of its options`
803782
)
804-
} else if (tag === 'INPUT' && type === 'checkbox') {
783+
} else if (tagName === 'INPUT' && type === 'checkbox') {
805784
throwError(
806785
`wrapper.setSelected() cannot be called on a <input ` +
807786
`type="checkbox" /> element. Use ` +
808787
`wrapper.setChecked() instead`
809788
)
810-
} else if (tag === 'INPUT' && type === 'radio') {
789+
} else if (tagName === 'INPUT' && type === 'radio') {
811790
throwError(
812791
`wrapper.setSelected() cannot be called on a <input ` +
813792
`type="radio" /> element. Use wrapper.setChecked() ` +
814793
`instead`
815794
)
816-
} else if (tag === 'INPUT' || tag === 'textarea') {
795+
} else if (tagName === 'INPUT' || tagName === 'textarea') {
817796
throwError(
818797
`wrapper.setSelected() cannot be called on "text" ` +
819798
`inputs. Use wrapper.setValue() instead`
820799
)
821800
} else {
822-
throwError(`wrapper.setSelected() cannot be called on this ` + `element`)
801+
throwError(`wrapper.setSelected() cannot be called on this element`)
823802
}
824803
}
825804

826805
/**
827806
* Return text of wrapper element
828807
*/
829808
text (): string {
830-
if (!this.element) {
831-
throwError(
832-
`cannot call wrapper.text() on a wrapper without an ` + `element`
833-
)
834-
}
835-
836809
return this.element.textContent.trim()
837810
}
838811

@@ -859,12 +832,6 @@ export default class Wrapper implements BaseWrapper {
859832
throwError('wrapper.trigger() must be passed a string')
860833
}
861834

862-
if (!this.element) {
863-
throwError(
864-
`cannot call wrapper.trigger() on a wrapper without ` + `an element`
865-
)
866-
}
867-
868835
if (options.target) {
869836
throwError(
870837
`you cannot set the target value of an event. See ` +

0 commit comments

Comments
 (0)