Skip to content

Commit

Permalink
createLocalVue errorHandler Option (#1670)
Browse files Browse the repository at this point in the history
* refactor(createlocalvue): move createLocalVue to shared utils

* refactor(createlocalvue): rename createLocalVue to _createLocalVue

Rename createLocalVue to _createLocalVue to indicate private use

* improvement(components): add Sync and Async components for testing

* improvement(flow): add VueConfig to Flow

* improvement(index): export new createLocalVue as default in Index

* improvement(_createlocalvue): allow registration of user defined config

Allow VueConfig to be passed in via createLocalVue to be registered on the created vue instance

* improvement(find): add findAllParentInstances to the find API

add findAllParentInstances method to traverse a component's parent to find globally registered
properties via createLocalVue

* improvement(mount): pass localVue into mounted createLocalVue

Pass localVue into mounted createLocalVue to register localVue properties on component

* improvement(error): call user defined errorHandler if defined

Call the user defined errorHandler created on the localVue instance via createLocalVue. This is
called in the VTU global error handler

* improvement(createlocalvue): add tests to createLocalVue errorHandler

add tests to createLocalVue errorHandler to test invocation on sync and async throws

* docs(createlocalvue): document public createLocalVue API

* docs(createlocalvue): document the createLocalVue internal API

* docs(createlocalvue): document the errorHandler option in createLocalVue

* fix(createlocalvue tests): wrap createLocalVue async test in try/finally

Wrap async error test for createLocalVue errorHandler in try/finally to prevent global errors

* fix(createlocalvue): skip async component throws for vue < 2.6

* improvement(find and error): add additional type safety to find & error

Add additional type safety to find and error for older versions of vue that might propagate a vm
value of null/undefined

* fix(createlocalvue): only run sync error tests for vue versions < 2.4
  • Loading branch information
AtofStryker committed Sep 10, 2020
1 parent 085ef2a commit e91effe
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 59 deletions.
29 changes: 29 additions & 0 deletions docs/api/createLocalVue.md
@@ -1,5 +1,10 @@
## createLocalVue()

- **Arguments:**

- `{Object} options`
- `{Function} errorHandler`

- **Returns:**

- `{Component}`
Expand All @@ -8,8 +13,12 @@

`createLocalVue` returns a Vue class for you to add components, mixins and install plugins without polluting the global Vue class.

The `errorHandler` option can be used to handle uncaught errors during component render function and watchers.

Use it with `options.localVue`:

**Without options:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import MyPlugin from 'my-plugin'
Expand All @@ -27,4 +36,24 @@ const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
```

**With the [`errorHandler`](https://vuejs.org/v2/api/#errorHandler) option:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const errorHandler = (err, vm, info) => {
expect(err).toBeInstanceOf(Error)
}

const localVue = createLocalVue({
errorHandler
})

// Foo throws an error inside a lifecycle hook
const wrapper = shallowMount(Foo, {
localVue
})
```

- **See also:** [Common Tips](../guides/common-tips.md#applying-global-plugins-and-mixins)
29 changes: 29 additions & 0 deletions docs/ja/api/createLocalVue.md
@@ -1,5 +1,10 @@
## createLocalVue()

- **引数:**

- `{Object} options`
- `{Function} errorHandler`

- **戻り値:**

- `{Component}`
Expand All @@ -8,8 +13,12 @@

`createLocalVue` は、グローバル Vue クラスを汚染することなくコンポーネント、ミックスイン、プラグインを追加するための Vue クラスを返します。

[errorHandler](https://jp.vuejs.org/v2/api/index.html#errorHandler)オプションを使用すると、コンポーネントのレンダー機能とウォッチャー中にキャッチされなかったエラーを処理できます。

`options.localVue` と一緒に使用してください。

**オプションなし:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'
Expand All @@ -25,4 +34,24 @@ const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
```

**[errorHandler](https://jp.vuejs.org/v2/api/index.html#errorHandler)オプションを使用:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const errorHandler = (err, vm, info) => {
expect(err).toBeInstanceOf(Error)
}

const localVue = createLocalVue({
errorHandler
})

// Fooがライフサイクルフック内でエラーをスローする
const wrapper = shallowMount(Foo, {
localVue
})
```

- **参照:** [一般的なヒント](../guides/common-tips.md#グローバルプラグインとミックスインの適用)
29 changes: 29 additions & 0 deletions docs/ru/api/createLocalVue.md
@@ -1,5 +1,10 @@
## createLocalVue()

- **Аргументы:**

- `{Object} options`
- `{Function} errorHandler`

- **Возвращает:**

- `{Component}`
Expand All @@ -8,8 +13,12 @@

`createLocalVue` возвращает класс Vue, чтобы вы могли добавлять компоненты, примеси и устанавливать плагины без загрязнения глобального класса Vue.

Опция [errorHandler](https://ru.vuejs.org/v2/api/index.html#errorHandler) может использоваться для обработки неперехваченных ошибок во время функции визуализации компонента и наблюдателей.

Используйте вместе с `options.localVue`:

**Без опций:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'
Expand All @@ -25,4 +34,24 @@ const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
```

**С опцией [errorHandler](https://ru.vuejs.org/v2/api/index.html#errorHandler):**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const errorHandler = (err, vm, info) => {
expect(err).toBeInstanceOf(Error)
}

const localVue = createLocalVue({
errorHandler
})

// Foo выдает ошибку внутри ловушки жизненного цикла
const wrapper = shallowMount(Foo, {
localVue
})
```

- **См. также:** [Общие советы](../guides/common-tips.md#добавnение-гnобаnьных-пnагинов-и-примесей)
29 changes: 29 additions & 0 deletions docs/zh/api/createLocalVue.md
@@ -1,5 +1,10 @@
## createLocalVue()

- **参数:**

- `{Object} options`
- `{Function} errorHandler`

- **返回值:**

- `{Component}`
Expand All @@ -8,8 +13,12 @@

`createLocalVue` 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。

在组件渲染功能和观察者期间,[`errorHandler`](https://cn.vuejs.org/v2/api/index.html#errorHandler)选项可用于处理未捕获的错误。

可通过 `options.localVue` 来使用:

**Without options:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import MyPlugin from 'my-plugin'
Expand All @@ -27,4 +36,24 @@ const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
```

**使用[`errorHandler`](https://cn.vuejs.org/v2/api/index.html#errorHandler)选项:**

```js
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const errorHandler = (err, vm, info) => {
expect(err).toBeInstanceOf(Error)
}

const localVue = createLocalVue({
errorHandler
})

// Foo在生命周期挂钩中引发错误
const wrapper = shallowMount(Foo, {
localVue
})
```

- **延伸阅读:**[常用技巧](../guides/common-tips.md#applying-global-plugins-and-mixins)
4 changes: 4 additions & 0 deletions flow/options.flow.js
Expand Up @@ -36,6 +36,10 @@ declare type NormalizedOptions = {
shouldProxy?: boolean
}

declare type VueConfig = {
errorHandler?: Function
}

declare type SlotValue = Component | string | Array<Component | string>

declare type SlotsObject = { [name: string]: SlotValue }
Expand Down
4 changes: 2 additions & 2 deletions packages/server-test-utils/src/renderToString.js
Expand Up @@ -6,7 +6,7 @@ import { throwError } from 'shared/util'
import { createRenderer } from 'vue-server-renderer'
import { mergeOptions } from 'shared/merge-options'
import config from './config'
import testUtils from '@vue/test-utils'
import _createLocalVue from 'shared/create-local-vue'
import { validateOptions } from 'shared/validate-options'

Vue.config.productionTip = false
Expand Down Expand Up @@ -34,7 +34,7 @@ export default function renderToString(
const vm = createInstance(
component,
mergedOptions,
testUtils.createLocalVue(options.localVue)
_createLocalVue(options.localVue)
)

return renderer.renderToString(vm)
Expand Down
67 changes: 67 additions & 0 deletions packages/shared/create-local-vue.js
@@ -0,0 +1,67 @@
// @flow

import Vue from 'vue'
import cloneDeep from 'lodash/cloneDeep'

/**
* Used internally by vue-server-test-utils and test-utils to propagate/create vue instances.
* This method is wrapped by createLocalVue in test-utils to provide a different public API signature
* @param {Component} _Vue
* @param {VueConfig} config
* @returns {Component}
*/
function _createLocalVue(
_Vue: Component = Vue,
config: VueConfig = {}
): Component {
const instance = _Vue.extend()

// clone global APIs
Object.keys(_Vue).forEach(key => {
if (!instance.hasOwnProperty(key)) {
const original = _Vue[key]
// cloneDeep can fail when cloning Vue instances
// cloneDeep checks that the instance has a Symbol
// which errors in Vue < 2.17 (https://github.com/vuejs/vue/pull/7878)
try {
instance[key] =
typeof original === 'object' ? cloneDeep(original) : original
} catch (e) {
instance[key] = original
}
}
})

// config is not enumerable
instance.config = cloneDeep(Vue.config)

// if a user defined errorHandler is defined by a localVue instance via createLocalVue, register it
instance.config.errorHandler = config.errorHandler || Vue.config.errorHandler

// option merge strategies need to be exposed by reference
// so that merge strats registered by plugins can work properly
instance.config.optionMergeStrategies = Vue.config.optionMergeStrategies

// make sure all extends are based on this instance.
// this is important so that global components registered by plugins,
// e.g. router-link are created using the correct base constructor
instance.options._base = instance

// compat for vue-router < 2.7.1 where it does not allow multiple installs
if (instance._installedPlugins && instance._installedPlugins.length) {
instance._installedPlugins.length = 0
}
const use = instance.use
instance.use = (plugin, ...rest) => {
if (plugin.installed === true) {
plugin.installed = false
}
if (plugin.install && plugin.install.installed === true) {
plugin.install.installed = false
}
use.call(instance, plugin, ...rest)
}
return instance
}

export default _createLocalVue
60 changes: 9 additions & 51 deletions packages/test-utils/src/create-local-vue.js
@@ -1,56 +1,14 @@
// @flow

import Vue from 'vue'
import cloneDeep from 'lodash/cloneDeep'

function createLocalVue(_Vue: Component = Vue): Component {
const instance = _Vue.extend()

// clone global APIs
Object.keys(_Vue).forEach(key => {
if (!instance.hasOwnProperty(key)) {
const original = _Vue[key]
// cloneDeep can fail when cloning Vue instances
// cloneDeep checks that the instance has a Symbol
// which errors in Vue < 2.17 (https://github.com/vuejs/vue/pull/7878)
try {
instance[key] =
typeof original === 'object' ? cloneDeep(original) : original
} catch (e) {
instance[key] = original
}
}
})

// config is not enumerable
instance.config = cloneDeep(Vue.config)

instance.config.errorHandler = Vue.config.errorHandler

// option merge strategies need to be exposed by reference
// so that merge strats registered by plugins can work properly
instance.config.optionMergeStrategies = Vue.config.optionMergeStrategies

// make sure all extends are based on this instance.
// this is important so that global components registered by plugins,
// e.g. router-link are created using the correct base constructor
instance.options._base = instance

// compat for vue-router < 2.7.1 where it does not allow multiple installs
if (instance._installedPlugins && instance._installedPlugins.length) {
instance._installedPlugins.length = 0
}
const use = instance.use
instance.use = (plugin, ...rest) => {
if (plugin.installed === true) {
plugin.installed = false
}
if (plugin.install && plugin.install.installed === true) {
plugin.install.installed = false
}
use.call(instance, plugin, ...rest)
}
return instance
import _createLocalVue from 'shared/create-local-vue'

/**
* Returns a local vue instance to add components, mixins and install plugins without polluting the global Vue class
* @param {VueConfig} config
* @returns {Component}
*/
function createLocalVue(config: VueConfig = {}): Component {
return _createLocalVue(undefined, config)
}

export default createLocalVue
23 changes: 21 additions & 2 deletions packages/test-utils/src/error.js
@@ -1,14 +1,33 @@
import { warn } from 'shared/util'
import { findAllInstances } from './find'
import { findAllInstances, findAllParentInstances } from './find'

function errorHandler(errorOrString, vm) {
function errorHandler(errorOrString, vm, info) {
const error =
typeof errorOrString === 'object' ? errorOrString : new Error(errorOrString)

// If a user defined errorHandler was register via createLocalVue
// find and call the user defined errorHandler
const instancedErrorHandlers = findAllParentInstances(vm)
.filter(
_vm =>
_vm &&
_vm.$options &&
_vm.$options.localVue &&
_vm.$options.localVue.config &&
_vm.$options.localVue.config.errorHandler
)
.map(_vm => _vm.$options.localVue.config.errorHandler)

if (vm) {
vm._error = error
}

// should be one error handler, as only once can be registered with local vue
// regardless, if more exist (for whatever reason), invoke the other user defined error handlers
instancedErrorHandlers.forEach(instancedErrorHandler => {
instancedErrorHandler(error, vm, info)
})

throw error
}

Expand Down

0 comments on commit e91effe

Please sign in to comment.