Skip to content

Commit

Permalink
chore: add translation for nextTick part (#1475)
Browse files Browse the repository at this point in the history
This resolve /#1451
  • Loading branch information
zkd8907 committed Mar 18, 2020
1 parent e2a6986 commit aff87f8
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 44 deletions.
34 changes: 19 additions & 15 deletions docs/zh/api/wrapper/emitted.md
Expand Up @@ -9,26 +9,30 @@
```js
import { mount } from '@vue/test-utils'

const wrapper = mount(Component)
test('emit demo', async () => {
const wrapper = mount(Component)

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted() 返回如下对象:
{
foo: [[], [123]]
}
*/
await wrapper.vm.$nextTick() // 等待事件处理完成

// 断言事件已经被触发
expect(wrapper.emitted().foo).toBeTruthy()
/*
wrapper.emitted() 返回如下对象:
{
foo: [[], [123]]
}
*/

// 断言事件的数量
expect(wrapper.emitted().foo.length).toBe(2)
// 断言事件已经被触发
expect(wrapper.emitted().foo).toBeTruthy()

// 断言事件的有效数据
expect(wrapper.emitted().foo[1]).toEqual([123])
// 断言事件的数量
expect(wrapper.emitted().foo.length).toBe(2)

// 断言事件的数量
expect(wrapper.emitted().foo[1]).toEqual([123])
})
```

你也可以把上面的代码写成这样:
Expand Down
28 changes: 16 additions & 12 deletions docs/zh/api/wrapper/trigger.md
Expand Up @@ -16,22 +16,26 @@ import { mount } from '@vue/test-utils'
import sinon from 'sinon'
import Foo from './Foo'

const clickHandler = sinon.stub()
const wrapper = mount(Foo, {
propsData: { clickHandler }
})
test('trigger demo', async () => {
const clickHandler = sinon.stub()
const wrapper = mount(Foo, {
propsData: { clickHandler }
})

wrapper.trigger('click')
wrapper.trigger('click')

wrapper.trigger('click', {
button: 0
})
wrapper.trigger('click', {
button: 0
})

wrapper.trigger('click', {
ctrlKey: true // 用于测试 @click.ctrl 处理函数
})
wrapper.trigger('click', {
ctrlKey: true // 用于测试 @click.ctrl 处理函数
})

expect(clickHandler.called).toBe(true)
await wrapper.vm.$nextTick() // 等待事件处理完成

expect(clickHandler.called).toBe(true)
})
```

- **设置事件目标:**
Expand Down
144 changes: 144 additions & 0 deletions docs/zh/guides/common-tips.md
Expand Up @@ -27,6 +27,50 @@ const wrapper = shallowMount(Component)
wrapper.vm // 挂载的 Vue 实例
```

### 生命周期钩子

在使用 `mount``shallowMount` 方法时,你可以期望你的组件响应 Vue 所有生命周期事件。但是请务必注意的是,除非使用 `Wrapper.destory()`,否则 `beforeDestroy``destroyed` _将不会触发_

此外组件在每个测试规范结束时并不会被自动销毁,并且将由用户来决定是否要存根或手动清理那些在测试规范结束前继续运行的任务( 例如 `setInterval` 或者 `setTimeout`)。

### 使用 `nextTick` 编写异步测试代码 (新)

默认情况下 Vue 会异步地批量执行更新(在下一轮 tick),以避免不必要的 DOM 重绘或者是观察者计算([查看文档](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue) 了解更多信息)。

这意味着你在更新会引发 DOM 变化的属性后**必须**等待一下。你可以使用 `Vue.nextTick()`

```js
it('updates text', async () => {
const wrapper = mount(Component)
wrapper.trigger('click')
await Vue.nextTick()
expect(wrapper.text()).toContain('updated')
})

// 或者你不希望使用async/await
it('render text', done => {
const wrapper = mount(TestComponent)
wrapper.trigger('click')
Vue.nextTick(() => {
wrapper.text().toContain('some text')
wrapper.trigger('click')
Vue.nextTick(() => {
wrapper.text().toContain('some different text')
done()
})
})
})
```

下面的方法通常会导致观察者更新,你需要等待下一轮 tick:

- `setChecked`
- `setData`
- `setSelected`
- `setProps`
- `setValue`
- `trigger`

### 断言触发的事件

每个挂载的包裹器都会通过其背后的 Vue 实例自动记录所有被触发的事件。你可以用 `wrapper.emitted()` 方法取回这些事件记录。
Expand Down Expand Up @@ -136,6 +180,106 @@ mount(Component, {

_想查阅所有选项的完整列表,请移步该文档的[挂载选项](../api/options.md)章节。_

### 仿造 Transitions

尽管在大多数情况下使用 `await Vue.nextTick()` 效果很好,但是在某些情况下还需要一些额外的工作。这些问题将在 `vue-test-utils` 移出 beta 版本之前解决。其中一个例子是 Vue 提供的带有 `<transition>` 包装器的单元测试组件。

```vue
<template>
<div>
<transition>
<p v-if="show">Foo</p>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
}
}
</script>
```

您可能想编写一个测试用例来验证是否显示了文本 Foo ,在将 `show` 设置为 `false` 时,不再显示文本 Foo。测试用例可以这么写:

```js
test('should render Foo, then hide it', async () => {
const wrapper = mount(Foo)
expect(wrapper.text()).toMatch(/Foo/)

wrapper.setData({
show: false
})
await wrapper.vm.$nextTick()

expect(wrapper.text()).not.toMatch(/Foo/)
})
```

实际上,尽管我们调用了 `setData` 方法,然后等待 `nextTick` 来确保 DOM 被更新,但是该测试用例仍然失败了。这是一个已知的问题,与 Vue 中 `<transition>` 组件的实现有关,我们希望在 1.0 版之前解决该问题。在目前情况下,有一些解决方案:

#### 使用 `transitionStub`

```js
const transitionStub = () => ({
render: function(h) {
return this.$options._renderChildren
}
})

test('should render Foo, then hide it', async () => {
const wrapper = mount(Foo, {
stubs: {
transition: transitionStub()
}
})
expect(wrapper.text()).toMatch(/Foo/)

wrapper.setData({
show: false
})
await wrapper.vm.$nextTick()

expect(wrapper.text()).not.toMatch(/Foo/)
})
```

上面的代码重写了 `<transition>` 组件的默认行为,并在在条件发生变化时立即呈现子元素。这与 Vue 中 `<transition>` 组件应用 CSS 类的实现是相反的。

#### 避免 `setData`

另一种选择是通过编写两个测试来简单地避免使用 `setData`,这要求我们在使用 `mount` 或者 `shallowMount` 时需要指定一些  选项:

```js
test('should render Foo', async () => {
const wrapper = mount(Foo, {
data() {
return {
show: true
}
}
})

expect(wrapper.text()).toMatch(/Foo/)
})

test('should not render Foo', async () => {
const wrapper = mount(Foo, {
data() {
return {
show: false
}
}
})

expect(wrapper.text()).not.toMatch(/Foo/)
})
```

### 应用全局的插件和混入

有些组件可能依赖一个全局插件或混入 (mixin) 的功能注入,比如 `vuex``vue-router`
Expand Down
31 changes: 25 additions & 6 deletions docs/zh/guides/getting-started.md
Expand Up @@ -102,15 +102,29 @@ it('button click should increment the count', () => {
})
```

### 关于 `nextTick` 怎么办?
为了测试计数器中的文本是否已经更新,我们需要了解 `nextTick`

Vue 会异步的将未生效的 DOM 更新批量应用,以避免因数据反复突变而导致的无谓的重渲染。这也是为什么在实践过程中我们经常在触发状态改变后用 `Vue.nextTick` 来等待 Vue 把实际的 DOM 更新做完的原因。
### 使用 `nextTick`

为了简化用法,Vue Test Utils 同步应用了所有的更新,所以你不需要在测试中使用 `Vue.nextTick` 来等待 DOM 更新
Vue 会异步的将未生效的 DOM 批量更新,避免因数据反复变化而导致不必要的渲染

_注意:当你需要为诸如异步回调或 Promise 解析等操作显性改进为事件循环的时候,`nextTick` 仍然是必要的_
_你可以阅读[Vue 文档](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)了解更多关于异步指更新的信息_

如果你仍然需要在自己的测试文件中使用 `nextTick`,注意任何在其内部被抛出的错误可能都不会被测试运行器捕获,因为其内部使用了 Promise。关于这个问题有两个建议:要么你可以在测试的一开始将 Vue 的全局错误处理器设置为 `done` 回调,要么你可以在调用 `nextTick` 时不带参数让其作为一个 Promise 返回:
更新会引发 DOM 变化的属性后,我们需要使用 `Vue.nextTick()` 等待 Vue 完成 DOM 更新。

在编写测试代码时,我们可以在异步函数里使用`await` `Vue.nextTick()`

```js
it('button click should increment the count text', async () => {
expect(wrapper.text()).toContain('0')
const button = wrapper.find('button')
button.trigger('click')
await Vue.nextTick()
expect(wrapper.text()).toContain('1')
})
```

当你在测试代码中使用 `nextTick` 时,请注意任何在其内部被抛出的错误可能都不会被测试运行器捕获,因为其内部使用了 Promise。关于这个问题有两个建议:要么你可以在测试的一开始将 Vue 的全局错误处理器设置为 `done` 回调,要么你可以在调用 `nextTick` 时不带参数让其作为一个 Promise 返回:

```js
// 这不会被捕获
Expand All @@ -121,7 +135,7 @@ it('will time out', done => {
})
})

// 接下来的两项测试都会如预期工作
// 接下来的三项测试都会如预期工作
it('will catch the error using done', done => {
Vue.config.errorHandler = done
Vue.nextTick(() => {
Expand All @@ -135,6 +149,11 @@ it('will catch the error using a promise', () => {
expect(true).toBe(false)
})
})

it('will catch the error using async/await', async () => {
await Vue.nextTick()
expect(true).toBe(false)
})
```

### 下一步是什么
Expand Down
51 changes: 41 additions & 10 deletions docs/zh/guides/testing-async-components.md
@@ -1,18 +1,49 @@
## 测试异步行为

为了让测试变得简单,`@vue/test-utils` *同步*应用 DOM 更新。不过当测试一个带有回调或 Promise 等异步行为的组件时,你需要留意一些技巧。
在编写测试代码时你将会遇到两种异步行为:

API 调用和 Vuex action 都是最常见的异步行为之一。下列例子展示了如何测试一个会调用到 API 的方法。这个例子使用 Jest 运行测试用例同时模拟了 HTTP 库 `axios`。更多关于 Jest 的手动模拟的介绍可移步[这里](https://jestjs.io/docs/zh-Hans/manual-mocks)
1. 来自 Vue 的更新
2. 来自外部行为的更新

`axios` 的模拟实现大概是这个样子的:
## 来自 Vue 的更新

Vue 会异步的将未生效的 DOM 批量更新,避免因数据反复变化而导致不必要的渲染。

_你可以阅读[Vue 文档](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)了解更多关于异步指更新的信息。_

在实践中,往往意味着你在更新会引发 DOM 变化的属性后必须使用 `Vue.nextTick()` 来等待 Vue 完成 DOM 更新。

使用 `Vue.nextTick()` 最简单的方法是在你的测试代码中使用异步函数:

```js
// 在文件头部引用Vue库
import Vue from 'vue'

// 其它的代码片断...

// 在测试框架中,编写一个测试用例
it('button click should increment the count text', async () => {
expect(wrapper.text()).toContain('0')
const button = wrapper.find('button')
button.trigger('click')
await Vue.nextTick()
expect(wrapper.text()).toContain('1')
})
```

## 来自外部行为的更新

在 Vue 之外最常见的一种异步行为就是在 Vuex 中进行 API 调用。以下示例将展示如何测试在 Vuex 中进行 API 调用的方法。本示例使用 Jest 运行测试并模拟 HTTP 库`axios`。可以在[这里](https://jestjs.io/docs/en/manual-mocks.html#content)找到有关 Jest Mock 的更多信息。

`axios` Mock 的实现如下所示:

```js
export default {
get: () => Promise.resolve({ data: 'value' })
}
```

下面的组件在按钮被点击的时候会调用一个 API,然后将响应的值赋给 `value`
当按钮被点击时,组件将会产生一个 API 调用,并且将响应的返回内容赋值给 `value`

```html
<template>
Expand All @@ -39,7 +70,7 @@ export default {
</script>
```

测试用例可以写成像这样
可以这样编写测试

```js
import { shallowMount } from '@vue/test-utils'
Expand All @@ -53,7 +84,7 @@ it('fetches async when a button is clicked', () => {
})
```

现在这则测试用例会失败,因为断言在 `fetchResults` 中的 Promise 完成之前就被调用了。大多数单元测试库都提供一个回调来使得运行期知道测试用例的完成时机。Jest 和 Mocha 都是用了 `done`。我们可以和 `$nextTick``setTimeout` 结合使用 `done` 来确保任何 Promise 都会在断言之前完成
上面的代码代码会执行失败,这是因为我们在 `fetchResults` 方法执行完毕前就对结果进行断言。绝大多数单元测试框架都会提供一个回调来通知你测试将在何时完成。Jest 和 Mocha 都使用`done` 这个方法。我们可以将 `done``$nextTick``setTimeout` 结合使用,以确保在进行断言前已经处理完所有的 Promise 回调

```js
it('fetches async when a button is clicked', done => {
Expand All @@ -66,11 +97,11 @@ it('fetches async when a button is clicked', done => {
})
```

`setTimeout` 允许测试通过的原因是 Promise 回调的 microtask 队列会在处理 `setTimeout` 的回调的任务队列之前先被处理。也就是说在 `setTimeout` 的回调运行的时候,任何 microtask 队列上的 Promise 回调都已经执行过了。另一方面 `$nextTick` 会安排一个 microtask,但是因为 microtask 队列的处理方式是先进先出,所以也会保证回调在作出断言时已经被执行。更多的解释请移步[这里](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)
setTimeout 也可以使测试通过的原因是,Promise 回调的微任务队列会排在 setTimeout 回调的微任务队列之前。这意味着当 setTimeout 回调执行时,微任务队列上的所有 Promise 回调已经被执行过了。另一方面`$nextTick` 也存在调度微任务的情况,但是由于微任务队列是先进先出的,因此也保证了在进行断言时已经处理完所有的 Promise 回调。请参阅[此处]https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)了解更多详细说明

另一个解决方案是使用一个 `async` 函数配合 npm `flush-promises``flush-promises` 会清除所有等待完成的 Promise 句柄。你可以 `await` `flushPromiese` 调用,以此清除等待中的 Promise 并改进你的测试用例的可读性
另外一个使用 `async` 方法的解决方案是使用 npm 仓库中的 `flush-promises``flush-promises` 会刷新所有处于 pending 状态或 resolved 状态的 Promise。你可以用 `await` 语句来等待 `flushPromises` 刷新 Promise 的状态,这样可以提升你代码的可读性

更新后的测试看起来像这样
修改以后的测试代码

```js
import { shallowMount } from '@vue/test-utils'
Expand All @@ -86,4 +117,4 @@ it('fetches async when a button is clicked', async () => {
})
```

相同的技巧可以被运用在同样默认返回一个 Promise 的 Vuex action 中
相同的技术细节也可以应用在处理 Vue Actions 上,默认情况下,它也会返回一个 Promise

0 comments on commit aff87f8

Please sign in to comment.