Skip to content

Commit db4ab8b

Browse files
authored
fix: prevent setProps infinite loop with immediate watchers (#1752)
* prevent `setProps` from being called on non-top level wrappers * remove useless `silent` option from config
1 parent e8b57a8 commit db4ab8b

File tree

18 files changed

+120
-178
lines changed

18 files changed

+120
-178
lines changed

Diff for: docs/api/config.md

+1-16
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ config.deprecationWarningHandler = (method, message) => {
4747
- type: `{ [name: string]: Component | boolean | string }`
4848
- default: `{}`
4949

50-
The stub stored in `config.stubs` is used by default.
50+
The stub stored in `config.stubs` is used by default.
5151
Stubs to use in components. These are overwritten by `stubs` passed in the mounting options.
5252

5353
When passing `stubs` as an array in the mounting options, `config.stubs` are converted to an array, and will stub components with a basic component that returns `<${component name}-stub>`.
@@ -112,18 +112,3 @@ config.provide['$logger'] = {
112112
}
113113
}
114114
```
115-
116-
### `silent`
117-
118-
- type: `Boolean`
119-
- default: `true`
120-
121-
It suppresses warnings triggered by Vue while mutating component's observables (e.g. props). When set to `false`, all warnings are visible in the console. This is a configurable way which relies on `Vue.config.silent`.
122-
123-
Example:
124-
125-
```js
126-
import { config } from '@vue/test-utils'
127-
128-
config.silent = false
129-
```

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
Sets `Wrapper` `vm` props and forces update.
1010

11-
**Note the Wrapper must contain a Vue instance.**
11+
::: warning
12+
`setProps` could be called only for top-level component, mounted by `mount` or `shallowMount`
13+
:::
1214

1315
```js
1416
import { mount } from '@vue/test-utils'

Diff for: docs/ja/api/config.md

-15
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,3 @@ config.provide['$logger'] = {
7373
}
7474
}
7575
```
76-
77-
### `silent`
78-
79-
- 型: `Boolean`
80-
- デフォルト: `true`
81-
82-
Vue がコンポーネントの変更を感知するプロパティ(例えば props )が変更される時に出す警告を出力しません。`false` をセットするとすべての警告はコンソールに表示されません。この機能は `Vue.config.silent` を使って実現しています。
83-
84-
例:
85-
86-
```js
87-
import { config } from '@vue/test-utils'
88-
89-
config.silent = false
90-
```

Diff for: docs/ru/api/config.md

-15
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,3 @@ config.provide['$logger'] = {
7474
}
7575
}
7676
```
77-
78-
### `silent`
79-
80-
- Тип: `Boolean`
81-
- По умолчанию: `true`
82-
83-
Подавляет предупреждения, вызванные Vue во время изменения наблюдаемых компонентов (например, входных параметров). Если установлено значение `false`, все предупреждения показываются в консоли. Это настраиваемый способ, который основывается на `Vue.config.silent`.
84-
85-
Пример:
86-
87-
```js
88-
import { config } from '@vue/test-utils'
89-
90-
config.silent = false
91-
```

Diff for: docs/ru/api/wrapper/setProps.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
Устанавливает входные параметры `Wrapper` `vm` и выполняет принудительное обновление.
1010

11-
**Обратите внимание, что `Wrapper` должен содержать экземпляр Vue.**
11+
::: warning Обратите внимание!
12+
`setProps` может быть вызван только на `wrapper` верхнего уровня, который был создан с помощью `mount` или `shallowMount`
13+
:::
1214

1315
```js
1416
import { mount } from '@vue/test-utils'

Diff for: docs/zh/api/config.md

+1-16
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ config.showDeprecationWarnings = false
2424
- 类型:`{ [name: string]: Component | boolean | string }`
2525
- 默认值:`{}`
2626

27-
存储在 `config.stubs` 中的存根会被默认使用。
27+
存储在 `config.stubs` 中的存根会被默认使用。
2828
用到的组件存根。它们会被传入挂载选项的 `stubs` 覆写。
2929

3030
当把 `stubs` 作为一个数组传入挂载选项时,`config.stubs` 会被转换为一个数组,然后用只返回一个 `<${component name}-stub>` 的基础组件进行存根。
@@ -89,18 +89,3 @@ config.provide['$logger'] = {
8989
}
9090
}
9191
```
92-
93-
### `silent`
94-
95-
- 类型:`Boolean`
96-
- 默认值:`true`
97-
98-
在组件的可观察内容 (如 props) 发生突变时,警告会被 Vue 阻止。当设置为 `false` 时,所有的警告都会出现在控制台中。这是一个 `Vue.config.silent` 的配置方式。
99-
100-
示例;
101-
102-
```js
103-
import { config } from '@vue/test-utils'
104-
105-
config.silent = false
106-
```

Diff for: flow/config.flow.js

-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ declare type Config = {
33
mocks?: Object,
44
methods?: { [name: string]: Function },
55
provide?: Object,
6-
silent?: boolean,
76
showDeprecationWarnings?: boolean
87
}

Diff for: packages/create-instance/create-instance.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import createScopedSlots from './create-scoped-slots'
1111
import { createStubsFromStubsObject } from './create-component-stubs'
1212
import { patchCreateElement } from './patch-create-element'
1313

14-
function createContext(options, scopedSlots) {
14+
function createContext(options, scopedSlots, currentProps) {
1515
const on = {
1616
...(options.context && options.context.on),
1717
...options.listeners
@@ -20,8 +20,8 @@ function createContext(options, scopedSlots) {
2020
attrs: {
2121
...options.attrs,
2222
// pass as attrs so that inheritAttrs works correctly
23-
// propsData should take precedence over attrs
24-
...options.propsData
23+
// props should take precedence over attrs
24+
...currentProps
2525
},
2626
...(options.context || {}),
2727
on,
@@ -110,16 +110,26 @@ export default function createInstance(
110110
parentComponentOptions.provide = function() {
111111
return {
112112
...getValuesFromCallableOption.call(this, originalParentComponentProvide),
113+
// $FlowIgnore
113114
...getValuesFromCallableOption.call(this, options.provide)
114115
}
115116
}
116117

118+
const originalParentComponentData = parentComponentOptions.data
119+
parentComponentOptions.data = function() {
120+
return {
121+
...getValuesFromCallableOption.call(this, originalParentComponentData),
122+
vueTestUtils_childProps: { ...options.propsData }
123+
}
124+
}
125+
117126
parentComponentOptions.$_doNotStubChildren = true
127+
parentComponentOptions.$_isWrapperParent = true
118128
parentComponentOptions._isFunctionalContainer = componentOptions.functional
119129
parentComponentOptions.render = function(h) {
120130
return h(
121131
Constructor,
122-
createContext(options, scopedSlots),
132+
createContext(options, scopedSlots, this.vueTestUtils_childProps),
123133
createChildren(this, h, options)
124134
)
125135
}

Diff for: packages/server-test-utils/types/index.d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ interface VueTestUtilsConfigOptions {
4545
mocks?: object
4646
methods?: Record<string, Function>
4747
provide?: object,
48-
silent?: Boolean
4948
}
5049

5150
export declare let config: VueTestUtilsConfigOptions

Diff for: packages/server-test-utils/types/test/renderToString.ts

-1
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,3 @@ config.methods = {
6666
config.provide = {
6767
foo: {}
6868
}
69-
config.silent = true

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

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export default {
66
mocks: {},
77
methods: {},
88
provide: {},
9-
silent: true,
109
showDeprecationWarnings:
1110
typeof process.env.SHOW_DEPRECATIONS !== 'undefined'
1211
? process.env.SHOW_DEPRECATIONS

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

+37-61
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
22

3-
import Vue from 'vue'
43
import pretty from 'pretty'
54
import getSelector from './get-selector'
65
import {
@@ -9,7 +8,6 @@ import {
98
VUE_VERSION,
109
DOM_SELECTOR
1110
} from 'shared/consts'
12-
import config from './config'
1311
import WrapperArray from './wrapper-array'
1412
import ErrorWrapper from './error-wrapper'
1513
import {
@@ -721,71 +719,49 @@ export default class Wrapper implements BaseWrapper {
721719
if (!this.vm) {
722720
throwError(`wrapper.setProps() can only be called on a Vue instance`)
723721
}
724-
this.__warnIfDestroyed()
725722

726-
// Save the original "silent" config so that we can directly mutate props
727-
const originalConfig = Vue.config.silent
728-
Vue.config.silent = config.silent
729-
730-
try {
731-
Object.keys(data).forEach(key => {
732-
// Don't let people set entire objects, because reactivity won't work
733-
if (
734-
typeof data[key] === 'object' &&
735-
data[key] !== null &&
736-
// $FlowIgnore : Problem with possibly null this.vm
737-
data[key] === this.vm[key]
738-
) {
739-
throwError(
740-
`wrapper.setProps() called with the same object of the existing ` +
741-
`${key} property. You must call wrapper.setProps() with a new ` +
742-
`object to trigger reactivity`
743-
)
744-
}
723+
// $FlowIgnore : Problem with possibly null this.vm
724+
if (!this.vm.$parent.$options.$_isWrapperParent) {
725+
throwError(
726+
`wrapper.setProps() can only be called for top-level component`
727+
)
728+
}
745729

746-
if (
747-
!this.vm ||
748-
!this.vm.$options._propKeys ||
749-
!this.vm.$options._propKeys.some(prop => prop === key)
750-
) {
751-
if (VUE_VERSION > 2.3) {
752-
// $FlowIgnore : Problem with possibly null this.vm
753-
this.vm.$attrs[key] = data[key]
754-
return nextTick()
755-
}
756-
throwError(
757-
`wrapper.setProps() called with ${key} property which ` +
758-
`is not defined on the component`
759-
)
760-
}
730+
this.__warnIfDestroyed()
761731

762-
// Actually set the prop
732+
Object.keys(data).forEach(key => {
733+
// Don't let people set entire objects, because reactivity won't work
734+
if (
735+
typeof data[key] === 'object' &&
736+
data[key] !== null &&
763737
// $FlowIgnore : Problem with possibly null this.vm
764-
this.vm[key] = data[key]
765-
})
738+
data[key] === this.vm[key]
739+
) {
740+
throwError(
741+
`wrapper.setProps() called with the same object of the existing ` +
742+
`${key} property. You must call wrapper.setProps() with a new ` +
743+
`object to trigger reactivity`
744+
)
745+
}
746+
747+
if (
748+
VUE_VERSION <= 2.3 &&
749+
(!this.vm ||
750+
!this.vm.$options._propKeys ||
751+
!this.vm.$options._propKeys.some(prop => prop === key))
752+
) {
753+
throwError(
754+
`wrapper.setProps() called with ${key} property which ` +
755+
`is not defined on the component`
756+
)
757+
}
766758

767759
// $FlowIgnore : Problem with possibly null this.vm
768-
this.vm.$forceUpdate()
769-
return new Promise(resolve => {
770-
nextTick().then(() => {
771-
const isUpdated = Object.keys(data).some(key => {
772-
return (
773-
// $FlowIgnore : Problem with possibly null this.vm
774-
this.vm[key] === data[key] ||
775-
// $FlowIgnore : Problem with possibly null this.vm
776-
(this.vm.$attrs && this.vm.$attrs[key] === data[key])
777-
)
778-
})
779-
return !isUpdated ? this.setProps(data).then(resolve()) : resolve()
780-
})
781-
})
782-
} catch (err) {
783-
throw err
784-
} finally {
785-
// Ensure you teardown the modifications you made to the user's config
786-
// After all the props are set, then reset the state
787-
Vue.config.silent = originalConfig
788-
}
760+
const parent = this.vm.$parent
761+
parent.$set(parent.vueTestUtils_childProps, key, data[key])
762+
})
763+
764+
return nextTick()
789765
}
790766

791767
/**

Diff for: packages/test-utils/types/index.d.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export interface Wrapper<V extends Vue | null> extends BaseWrapper {
8585
get (selector: string): Wrapper<Vue>
8686
get (selector: RefSelector): Wrapper<Vue>
8787
get (selector: NameSelector): Wrapper<Vue>
88-
88+
8989
getComponent<R extends Vue> (selector: VueClass<R>): Wrapper<R>
9090
getComponent<R extends Vue> (selector: ComponentOptions<R>): Wrapper<R>
9191
getComponent<Props = DefaultProps, PropDefs = PropsDefinition<Props>>(selector: FunctionalComponentOptions<Props, PropDefs>): Wrapper<Vue>
@@ -170,7 +170,6 @@ interface VueTestUtilsConfigOptions {
170170
mocks: Record<string, any>
171171
methods: Record<string, Function>
172172
provide?: Record<string, any>,
173-
silent?: Boolean,
174173
showDeprecationWarnings?: boolean
175174
deprecationWarningHandler?: Function
176175
}

Diff for: packages/test-utils/types/test/mount.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ config.provide = {
101101
config.provide['foo'] = {
102102
bar: {}
103103
}
104-
config.silent = true
105104
config.showDeprecationWarnings = false
106105

107106
// Check we can use default export
108-
VueTestUtils.config.silent = false
107+
VueTestUtils.config.showDeprecationWarnings = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<div>
3+
{{ prop1 }}
4+
</div>
5+
</template>
6+
7+
<script>
8+
export default {
9+
data: function() {
10+
return {
11+
data1: null
12+
}
13+
},
14+
props: ['prop1'],
15+
watch: {
16+
prop1: {
17+
handler() {
18+
this.data1 = this.prop1
19+
},
20+
immediate: true
21+
}
22+
}
23+
}
24+
</script>

0 commit comments

Comments
 (0)