Skip to content

Commit 707bd2a

Browse files
committed
feat: warn when operating on destroyed Vue component
1 parent a821908 commit 707bd2a

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

packages/test-utils/src/wrapper.js

+56
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getCheckedEvent,
1818
isPhantomJS,
1919
nextTick,
20+
warn,
2021
warnDeprecated
2122
} from 'shared/util'
2223
import { isElementVisible } from 'shared/is-visible'
@@ -82,14 +83,27 @@ export default class Wrapper implements BaseWrapper {
8283
}
8384
}
8485

86+
/**
87+
* Prints warning if component is destroyed
88+
*/
89+
__warnIfDestroyed() {
90+
if (!this.exists()) {
91+
warn('Operations on destroyed component are discouraged')
92+
}
93+
}
94+
8595
at(): void {
96+
this.__warnIfDestroyed()
97+
8698
throwError('at() must be called on a WrapperArray')
8799
}
88100

89101
/**
90102
* Returns an Object containing all the attribute/value pairs on the element.
91103
*/
92104
attributes(key?: string): { [name: string]: string } | string {
105+
this.__warnIfDestroyed()
106+
93107
const attributes = this.element.attributes
94108
const attributeMap = {}
95109
for (let i = 0; i < attributes.length; i++) {
@@ -104,6 +118,8 @@ export default class Wrapper implements BaseWrapper {
104118
* Returns an Array containing all the classes on the element
105119
*/
106120
classes(className?: string): Array<string> | boolean {
121+
this.__warnIfDestroyed()
122+
107123
const classAttribute = this.element.getAttribute('class')
108124
let classes = classAttribute ? classAttribute.split(' ') : []
109125
// Handle converting cssmodules identifiers back to the original class name
@@ -134,6 +150,9 @@ export default class Wrapper implements BaseWrapper {
134150
'contains',
135151
'Use `wrapper.find`, `wrapper.findComponent` or `wrapper.get` instead'
136152
)
153+
154+
this.__warnIfDestroyed()
155+
137156
const selector = getSelector(rawSelector, 'contains')
138157
const nodes = find(this.rootNode, this.vm, selector)
139158
return nodes.length > 0
@@ -209,6 +228,8 @@ export default class Wrapper implements BaseWrapper {
209228
* matches the provided selector.
210229
*/
211230
get(rawSelector: Selector): Wrapper {
231+
this.__warnIfDestroyed()
232+
212233
const found = this.find(rawSelector)
213234
if (found instanceof ErrorWrapper) {
214235
throw new Error(`Unable to find ${rawSelector} within: ${this.html()}`)
@@ -221,6 +242,8 @@ export default class Wrapper implements BaseWrapper {
221242
* matches the provided selector.
222243
*/
223244
find(rawSelector: Selector): Wrapper | ErrorWrapper {
245+
this.__warnIfDestroyed()
246+
224247
const selector = getSelector(rawSelector, 'find')
225248
if (selector.type !== DOM_SELECTOR) {
226249
warnDeprecated(
@@ -237,6 +260,8 @@ export default class Wrapper implements BaseWrapper {
237260
* matches the provided selector.
238261
*/
239262
findComponent(rawSelector: Selector): Wrapper | ErrorWrapper {
263+
this.__warnIfDestroyed()
264+
240265
const selector = getSelector(rawSelector, 'findComponent')
241266
if (!this.vm && !this.isFunctionalComponent) {
242267
throwError(
@@ -270,6 +295,8 @@ export default class Wrapper implements BaseWrapper {
270295
* the provided selector.
271296
*/
272297
findAll(rawSelector: Selector): WrapperArray {
298+
this.__warnIfDestroyed()
299+
273300
const selector = getSelector(rawSelector, 'findAll')
274301
if (selector.type !== DOM_SELECTOR) {
275302
warnDeprecated(
@@ -285,6 +312,8 @@ export default class Wrapper implements BaseWrapper {
285312
* the provided selector.
286313
*/
287314
findAllComponents(rawSelector: Selector): WrapperArray {
315+
this.__warnIfDestroyed()
316+
288317
const selector = getSelector(rawSelector, 'findAll')
289318
if (!this.vm) {
290319
throwError(
@@ -318,13 +347,17 @@ export default class Wrapper implements BaseWrapper {
318347
* Returns HTML of element as a string
319348
*/
320349
html(): string {
350+
this.__warnIfDestroyed()
351+
321352
return pretty(this.element.outerHTML)
322353
}
323354

324355
/**
325356
* Checks if node matches selector or component definition
326357
*/
327358
is(rawSelector: Selector): boolean {
359+
this.__warnIfDestroyed()
360+
328361
const selector = getSelector(rawSelector, 'is')
329362

330363
if (selector.type === DOM_SELECTOR) {
@@ -351,6 +384,8 @@ export default class Wrapper implements BaseWrapper {
351384
'Consider a custom matcher such as those provided in jest-dom: https://github.com/testing-library/jest-dom#tobeempty. ' +
352385
'When using with findComponent, access the DOM element with findComponent(Comp).element'
353386
)
387+
this.__warnIfDestroyed()
388+
354389
if (!this.vnode) {
355390
return this.element.innerHTML === ''
356391
}
@@ -375,6 +410,8 @@ export default class Wrapper implements BaseWrapper {
375410
* Checks if node is visible
376411
*/
377412
isVisible(): boolean {
413+
this.__warnIfDestroyed()
414+
378415
return isElementVisible(this.element)
379416
}
380417

@@ -384,6 +421,8 @@ export default class Wrapper implements BaseWrapper {
384421
*/
385422
isVueInstance(): boolean {
386423
warnDeprecated(`isVueInstance`)
424+
this.__warnIfDestroyed()
425+
387426
return !!this.vm
388427
}
389428

@@ -393,6 +432,7 @@ export default class Wrapper implements BaseWrapper {
393432
*/
394433
name(): string {
395434
warnDeprecated(`name`)
435+
this.__warnIfDestroyed()
396436

397437
if (this.vm) {
398438
return (
@@ -416,6 +456,7 @@ export default class Wrapper implements BaseWrapper {
416456
*/
417457
overview(): void {
418458
warnDeprecated(`overview`)
459+
this.__warnIfDestroyed()
419460

420461
if (!this.vm) {
421462
throwError(`wrapper.overview() can only be called on a Vue instance`)
@@ -495,6 +536,7 @@ export default class Wrapper implements BaseWrapper {
495536
if (!this.vm) {
496537
throwError('wrapper.props() must be called on a Vue instance')
497538
}
539+
this.__warnIfDestroyed()
498540

499541
const props = {}
500542
const keys = this.vm && this.vm.$options._propKeys
@@ -519,6 +561,8 @@ export default class Wrapper implements BaseWrapper {
519561
* @deprecated
520562
*/
521563
setChecked(checked: boolean = true): Promise<*> {
564+
this.__warnIfDestroyed()
565+
522566
if (typeof checked !== 'boolean') {
523567
throwError('wrapper.setChecked() must be passed a boolean')
524568
}
@@ -568,6 +612,8 @@ export default class Wrapper implements BaseWrapper {
568612
* @deprecated
569613
*/
570614
setSelected(): Promise<void> {
615+
this.__warnIfDestroyed()
616+
571617
const tagName = this.element.tagName
572618

573619
if (tagName === 'SELECT') {
@@ -613,6 +659,8 @@ export default class Wrapper implements BaseWrapper {
613659
throwError(`wrapper.setData() can only be called on a Vue instance`)
614660
}
615661

662+
this.__warnIfDestroyed()
663+
616664
recursivelySetData(this.vm, this.vm, data)
617665
return nextTick()
618666
}
@@ -630,6 +678,8 @@ export default class Wrapper implements BaseWrapper {
630678
if (!this.vm) {
631679
throwError(`wrapper.setMethods() can only be called on a Vue instance`)
632680
}
681+
this.__warnIfDestroyed()
682+
633683
Object.keys(methods).forEach(key => {
634684
// $FlowIgnore : Problem with possibly null this.vm
635685
this.vm[key] = methods[key]
@@ -657,6 +707,7 @@ export default class Wrapper implements BaseWrapper {
657707
if (!this.vm) {
658708
throwError(`wrapper.setProps() can only be called on a Vue instance`)
659709
}
710+
this.__warnIfDestroyed()
660711

661712
// Save the original "silent" config so that we can directly mutate props
662713
const originalConfig = Vue.config.silent
@@ -730,6 +781,7 @@ export default class Wrapper implements BaseWrapper {
730781
const tagName = this.element.tagName
731782
// $FlowIgnore
732783
const type = this.attributes().type
784+
this.__warnIfDestroyed()
733785

734786
if (tagName === 'OPTION') {
735787
throwError(
@@ -782,13 +834,17 @@ export default class Wrapper implements BaseWrapper {
782834
* Return text of wrapper element
783835
*/
784836
text(): string {
837+
this.__warnIfDestroyed()
838+
785839
return this.element.textContent.trim()
786840
}
787841

788842
/**
789843
* Dispatches a DOM event on wrapper
790844
*/
791845
trigger(type: string, options: Object = {}): Promise<void> {
846+
this.__warnIfDestroyed()
847+
792848
if (typeof type !== 'string') {
793849
throwError('wrapper.trigger() must be passed a string')
794850
}

test/specs/wrapper/destroy.spec.js

+58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { describeWithShallowAndMount } from '~resources/utils'
2+
import { config } from 'packages/test-utils/src'
23

34
describeWithShallowAndMount('destroy', mountingMethod => {
5+
let originalConsoleError
6+
7+
beforeEach(() => {
8+
config.showDeprecationWarnings = true
9+
originalConsoleError = console.error
10+
console.error = jest.fn()
11+
})
12+
13+
afterEach(() => {
14+
console.error = originalConsoleError
15+
})
16+
417
it('triggers beforeDestroy ', () => {
518
const stub = jest.fn()
619
mountingMethod({
@@ -61,4 +74,49 @@ describeWithShallowAndMount('destroy', mountingMethod => {
6174
const wrapper = mountingMethod(TestComponent)
6275
expect(() => wrapper.destroy()).toThrow()
6376
})
77+
78+
const StubComponent = { props: ['a'], template: '<div><p></p></div>' }
79+
80+
;[
81+
['attributes'],
82+
['classes'],
83+
['isEmpty'],
84+
['isVisible'],
85+
['isVueInstance'],
86+
['name'],
87+
['overview'],
88+
['props'],
89+
['text'],
90+
['html'],
91+
['contains', ['p']],
92+
['get', ['p']],
93+
['find', ['p']],
94+
['findComponent', [StubComponent]],
95+
['findAll', [StubComponent]],
96+
['findAllComponents', [StubComponent]],
97+
['is', [StubComponent]],
98+
['setProps', [{ a: 1 }]],
99+
['setData', [{}]],
100+
['setMethods', [{}]],
101+
['trigger', ['test-event']]
102+
].forEach(([method, args = []]) => {
103+
it(`displays warning when ${method} is called on destroyed wrapper`, () => {
104+
config.showDeprecationWarnings = false
105+
const wrapper = mountingMethod(StubComponent)
106+
wrapper.destroy()
107+
wrapper[method](...args)
108+
109+
expect(console.error).toHaveBeenCalled()
110+
})
111+
})
112+
;['emitted', 'emittedByOrder', 'exists'].forEach(method => {
113+
it(`does not display warning when ${method} is called on destroyed wrapper`, () => {
114+
config.showDeprecationWarnings = false
115+
const wrapper = mountingMethod(StubComponent)
116+
wrapper.destroy()
117+
wrapper[method]()
118+
119+
expect(console.error).not.toHaveBeenCalled()
120+
})
121+
})
64122
})

0 commit comments

Comments
 (0)