Skip to content

Commit

Permalink
fix: prevent setProps infinite loop with immediate watchers (#1752)
Browse files Browse the repository at this point in the history
* prevent `setProps` from being called on non-top level wrappers
* remove useless `silent` option from config
  • Loading branch information
xanf committed Jan 5, 2021
1 parent e8b57a8 commit db4ab8b
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 178 deletions.
17 changes: 1 addition & 16 deletions docs/api/config.md
Expand Up @@ -47,7 +47,7 @@ config.deprecationWarningHandler = (method, message) => {
- type: `{ [name: string]: Component | boolean | string }`
- default: `{}`

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

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>`.
Expand Down Expand Up @@ -112,18 +112,3 @@ config.provide['$logger'] = {
}
}
```

### `silent`

- type: `Boolean`
- default: `true`

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`.

Example:

```js
import { config } from '@vue/test-utils'

config.silent = false
```
4 changes: 3 additions & 1 deletion docs/api/wrapper/setProps.md
Expand Up @@ -8,7 +8,9 @@

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

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

```js
import { mount } from '@vue/test-utils'
Expand Down
15 changes: 0 additions & 15 deletions docs/ja/api/config.md
Expand Up @@ -73,18 +73,3 @@ config.provide['$logger'] = {
}
}
```

### `silent`

- 型: `Boolean`
- デフォルト: `true`

Vue がコンポーネントの変更を感知するプロパティ(例えば props )が変更される時に出す警告を出力しません。`false` をセットするとすべての警告はコンソールに表示されません。この機能は `Vue.config.silent` を使って実現しています。

例:

```js
import { config } from '@vue/test-utils'

config.silent = false
```
15 changes: 0 additions & 15 deletions docs/ru/api/config.md
Expand Up @@ -74,18 +74,3 @@ config.provide['$logger'] = {
}
}
```

### `silent`

- Тип: `Boolean`
- По умолчанию: `true`

Подавляет предупреждения, вызванные Vue во время изменения наблюдаемых компонентов (например, входных параметров). Если установлено значение `false`, все предупреждения показываются в консоли. Это настраиваемый способ, который основывается на `Vue.config.silent`.

Пример:

```js
import { config } from '@vue/test-utils'

config.silent = false
```
4 changes: 3 additions & 1 deletion docs/ru/api/wrapper/setProps.md
Expand Up @@ -8,7 +8,9 @@

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

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

```js
import { mount } from '@vue/test-utils'
Expand Down
17 changes: 1 addition & 16 deletions docs/zh/api/config.md
Expand Up @@ -24,7 +24,7 @@ config.showDeprecationWarnings = false
- 类型:`{ [name: string]: Component | boolean | string }`
- 默认值:`{}`

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

当把 `stubs` 作为一个数组传入挂载选项时,`config.stubs` 会被转换为一个数组,然后用只返回一个 `<${component name}-stub>` 的基础组件进行存根。
Expand Down Expand Up @@ -89,18 +89,3 @@ config.provide['$logger'] = {
}
}
```

### `silent`

- 类型:`Boolean`
- 默认值:`true`

在组件的可观察内容 (如 props) 发生突变时,警告会被 Vue 阻止。当设置为 `false` 时,所有的警告都会出现在控制台中。这是一个 `Vue.config.silent` 的配置方式。

示例;

```js
import { config } from '@vue/test-utils'

config.silent = false
```
1 change: 0 additions & 1 deletion flow/config.flow.js
Expand Up @@ -3,6 +3,5 @@ declare type Config = {
mocks?: Object,
methods?: { [name: string]: Function },
provide?: Object,
silent?: boolean,
showDeprecationWarnings?: boolean
}
18 changes: 14 additions & 4 deletions packages/create-instance/create-instance.js
Expand Up @@ -11,7 +11,7 @@ import createScopedSlots from './create-scoped-slots'
import { createStubsFromStubsObject } from './create-component-stubs'
import { patchCreateElement } from './patch-create-element'

function createContext(options, scopedSlots) {
function createContext(options, scopedSlots, currentProps) {
const on = {
...(options.context && options.context.on),
...options.listeners
Expand All @@ -20,8 +20,8 @@ function createContext(options, scopedSlots) {
attrs: {
...options.attrs,
// pass as attrs so that inheritAttrs works correctly
// propsData should take precedence over attrs
...options.propsData
// props should take precedence over attrs
...currentProps
},
...(options.context || {}),
on,
Expand Down Expand Up @@ -110,16 +110,26 @@ export default function createInstance(
parentComponentOptions.provide = function() {
return {
...getValuesFromCallableOption.call(this, originalParentComponentProvide),
// $FlowIgnore
...getValuesFromCallableOption.call(this, options.provide)
}
}

const originalParentComponentData = parentComponentOptions.data
parentComponentOptions.data = function() {
return {
...getValuesFromCallableOption.call(this, originalParentComponentData),
vueTestUtils_childProps: { ...options.propsData }
}
}

parentComponentOptions.$_doNotStubChildren = true
parentComponentOptions.$_isWrapperParent = true
parentComponentOptions._isFunctionalContainer = componentOptions.functional
parentComponentOptions.render = function(h) {
return h(
Constructor,
createContext(options, scopedSlots),
createContext(options, scopedSlots, this.vueTestUtils_childProps),
createChildren(this, h, options)
)
}
Expand Down
1 change: 0 additions & 1 deletion packages/server-test-utils/types/index.d.ts
Expand Up @@ -45,7 +45,6 @@ interface VueTestUtilsConfigOptions {
mocks?: object
methods?: Record<string, Function>
provide?: object,
silent?: Boolean
}

export declare let config: VueTestUtilsConfigOptions
Expand Down
1 change: 0 additions & 1 deletion packages/server-test-utils/types/test/renderToString.ts
Expand Up @@ -66,4 +66,3 @@ config.methods = {
config.provide = {
foo: {}
}
config.silent = true
1 change: 0 additions & 1 deletion packages/test-utils/src/config.js
Expand Up @@ -6,7 +6,6 @@ export default {
mocks: {},
methods: {},
provide: {},
silent: true,
showDeprecationWarnings:
typeof process.env.SHOW_DEPRECATIONS !== 'undefined'
? process.env.SHOW_DEPRECATIONS
Expand Down
98 changes: 37 additions & 61 deletions packages/test-utils/src/wrapper.js
@@ -1,6 +1,5 @@
// @flow

import Vue from 'vue'
import pretty from 'pretty'
import getSelector from './get-selector'
import {
Expand All @@ -9,7 +8,6 @@ import {
VUE_VERSION,
DOM_SELECTOR
} from 'shared/consts'
import config from './config'
import WrapperArray from './wrapper-array'
import ErrorWrapper from './error-wrapper'
import {
Expand Down Expand Up @@ -721,71 +719,49 @@ export default class Wrapper implements BaseWrapper {
if (!this.vm) {
throwError(`wrapper.setProps() can only be called on a Vue instance`)
}
this.__warnIfDestroyed()

// Save the original "silent" config so that we can directly mutate props
const originalConfig = Vue.config.silent
Vue.config.silent = config.silent

try {
Object.keys(data).forEach(key => {
// Don't let people set entire objects, because reactivity won't work
if (
typeof data[key] === 'object' &&
data[key] !== null &&
// $FlowIgnore : Problem with possibly null this.vm
data[key] === this.vm[key]
) {
throwError(
`wrapper.setProps() called with the same object of the existing ` +
`${key} property. You must call wrapper.setProps() with a new ` +
`object to trigger reactivity`
)
}
// $FlowIgnore : Problem with possibly null this.vm
if (!this.vm.$parent.$options.$_isWrapperParent) {
throwError(
`wrapper.setProps() can only be called for top-level component`
)
}

if (
!this.vm ||
!this.vm.$options._propKeys ||
!this.vm.$options._propKeys.some(prop => prop === key)
) {
if (VUE_VERSION > 2.3) {
// $FlowIgnore : Problem with possibly null this.vm
this.vm.$attrs[key] = data[key]
return nextTick()
}
throwError(
`wrapper.setProps() called with ${key} property which ` +
`is not defined on the component`
)
}
this.__warnIfDestroyed()

// Actually set the prop
Object.keys(data).forEach(key => {
// Don't let people set entire objects, because reactivity won't work
if (
typeof data[key] === 'object' &&
data[key] !== null &&
// $FlowIgnore : Problem with possibly null this.vm
this.vm[key] = data[key]
})
data[key] === this.vm[key]
) {
throwError(
`wrapper.setProps() called with the same object of the existing ` +
`${key} property. You must call wrapper.setProps() with a new ` +
`object to trigger reactivity`
)
}

if (
VUE_VERSION <= 2.3 &&
(!this.vm ||
!this.vm.$options._propKeys ||
!this.vm.$options._propKeys.some(prop => prop === key))
) {
throwError(
`wrapper.setProps() called with ${key} property which ` +
`is not defined on the component`
)
}

// $FlowIgnore : Problem with possibly null this.vm
this.vm.$forceUpdate()
return new Promise(resolve => {
nextTick().then(() => {
const isUpdated = Object.keys(data).some(key => {
return (
// $FlowIgnore : Problem with possibly null this.vm
this.vm[key] === data[key] ||
// $FlowIgnore : Problem with possibly null this.vm
(this.vm.$attrs && this.vm.$attrs[key] === data[key])
)
})
return !isUpdated ? this.setProps(data).then(resolve()) : resolve()
})
})
} catch (err) {
throw err
} finally {
// Ensure you teardown the modifications you made to the user's config
// After all the props are set, then reset the state
Vue.config.silent = originalConfig
}
const parent = this.vm.$parent
parent.$set(parent.vueTestUtils_childProps, key, data[key])
})

return nextTick()
}

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/test-utils/types/index.d.ts
Expand Up @@ -85,7 +85,7 @@ export interface Wrapper<V extends Vue | null> extends BaseWrapper {
get (selector: string): Wrapper<Vue>
get (selector: RefSelector): Wrapper<Vue>
get (selector: NameSelector): Wrapper<Vue>

getComponent<R extends Vue> (selector: VueClass<R>): Wrapper<R>
getComponent<R extends Vue> (selector: ComponentOptions<R>): Wrapper<R>
getComponent<Props = DefaultProps, PropDefs = PropsDefinition<Props>>(selector: FunctionalComponentOptions<Props, PropDefs>): Wrapper<Vue>
Expand Down Expand Up @@ -170,7 +170,6 @@ interface VueTestUtilsConfigOptions {
mocks: Record<string, any>
methods: Record<string, Function>
provide?: Record<string, any>,
silent?: Boolean,
showDeprecationWarnings?: boolean
deprecationWarningHandler?: Function
}
Expand Down
3 changes: 1 addition & 2 deletions packages/test-utils/types/test/mount.ts
Expand Up @@ -101,8 +101,7 @@ config.provide = {
config.provide['foo'] = {
bar: {}
}
config.silent = true
config.showDeprecationWarnings = false

// Check we can use default export
VueTestUtils.config.silent = false
VueTestUtils.config.showDeprecationWarnings = false
24 changes: 24 additions & 0 deletions test/resources/components/component-with-watch-immediate.vue
@@ -0,0 +1,24 @@
<template>
<div>
{{ prop1 }}
</div>
</template>

<script>
export default {
data: function() {
return {
data1: null
}
},
props: ['prop1'],
watch: {
prop1: {
handler() {
this.data1 = this.prop1
},
immediate: true
}
}
}
</script>

0 comments on commit db4ab8b

Please sign in to comment.