Skip to content

Commit

Permalink
feat: element, vnode, vm, and options are read-only (#748)
Browse files Browse the repository at this point in the history
breaking change
  • Loading branch information
38elements authored and eddyerburgh committed Jun 23, 2018
1 parent fccb3cb commit b801c25
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 172 deletions.
8 changes: 4 additions & 4 deletions docs/api/wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion packages/test-utils/src/vue-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
167 changes: 67 additions & 100 deletions packages/test-utils/src/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Array<any>> };
_emittedByOrder: Array<{ name: string, args: Array<any> }>;
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 frozenOptions = Object.freeze(options)
// $FlowIgnore
Object.defineProperty(this, 'options', {
get: () => frozenOptions,
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]}`
)
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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 &&
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 <select> ` +
`element. Use wrapper.setSelected() instead`
)
} else if (tag === 'INPUT' && type === 'checkbox') {
} else if (tagName === 'INPUT' && type === 'checkbox') {
throwError(
`wrapper.setValue() cannot be called on a <input ` +
`type="checkbox" /> 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 <input ` +
`type="radio" /> 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`)
}
}

Expand All @@ -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 ` +
`<select> element. Use wrapper.setSelected() ` +
`instead`
)
} else if (tag === 'INPUT' && type === 'checkbox') {
} else if (tagName === 'INPUT' && type === 'checkbox') {
// $FlowIgnore
if (el.checked !== checked) {
if (this.element.checked !== checked) {
if (!navigator.userAgent.includes('jsdom')) {
// $FlowIgnore
el.checked = checked
this.element.checked = checked
}
this.trigger('click')
this.trigger(event)
this.trigger('change')
}
} else if (tag === 'INPUT' && type === 'radio') {
} else if (tagName === 'INPUT' && type === 'radio') {
if (!checked) {
throwError(
`wrapper.setChecked() cannot be called with ` +
Expand All @@ -752,87 +740,72 @@ export default class Wrapper implements BaseWrapper {
)
} else {
// $FlowIgnore
if (!el.checked) {
if (!this.element.checked) {
this.trigger('click')
this.trigger(event)
this.trigger('change')
}
}
} else if (tag === 'INPUT' || tag === 'textarea') {
} else if (tagName === 'INPUT' || tagName === 'textarea') {
throwError(
`wrapper.setChecked() cannot be called on "text" ` +
`inputs. Use wrapper.setValue() instead`
)
} else {
throwError(`wrapper.setChecked() cannot be called on this ` + `element`)
throwError(`wrapper.setChecked() cannot be called on this element`)
}
}

/**
* Selects <option></option> element
*/
setSelected () {
const el = this.element

if (!el) {
throwError(
`cannot call wrapper.setSelected() 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 === 'OPTION') {
if (tagName === 'OPTION') {
// $FlowIgnore
el.selected = true
this.element.selected = true
// $FlowIgnore
if (el.parentElement.tagName === 'OPTGROUP') {
if (this.element.parentElement.tagName === 'OPTGROUP') {
// $FlowIgnore
createWrapper(el.parentElement.parentElement, this.options).trigger(
event
)
createWrapper(this.element.parentElement.parentElement, this.options)
.trigger('change')
} else {
// $FlowIgnore
createWrapper(el.parentElement, this.options).trigger(event)
createWrapper(this.element.parentElement, this.options)
.trigger('change')
}
} else if (tag === 'SELECT') {
} else if (tagName === 'SELECT') {
throwError(
`wrapper.setSelected() cannot be called on select. ` +
`Call it on one of its options`
)
} else if (tag === 'INPUT' && type === 'checkbox') {
} else if (tagName === 'INPUT' && type === 'checkbox') {
throwError(
`wrapper.setSelected() cannot be called on a <input ` +
`type="checkbox" /> 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 <input ` +
`type="radio" /> 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`)
}
}

/**
* 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()
}

Expand All @@ -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 ` +
Expand Down
Loading

0 comments on commit b801c25

Please sign in to comment.