Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent setProps infinite loop with immediate watchers #1752

Merged
merged 1 commit into from Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not 100% sure but looks like a typo? ignore me if I'm wrong

Suggested change
`setProps` could be called only for top-level component, mounted by `mount` or `shallowMount`
`setProps` should 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>