diff --git a/docs/zh/api/wrapper/emitted.md b/docs/zh/api/wrapper/emitted.md index f8faa61e4..c1fe56f42 100644 --- a/docs/zh/api/wrapper/emitted.md +++ b/docs/zh/api/wrapper/emitted.md @@ -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]) +}) ``` 你也可以把上面的代码写成这样: diff --git a/docs/zh/api/wrapper/trigger.md b/docs/zh/api/wrapper/trigger.md index ba52e80da..861679de4 100644 --- a/docs/zh/api/wrapper/trigger.md +++ b/docs/zh/api/wrapper/trigger.md @@ -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) +}) ``` - **设置事件目标:** diff --git a/docs/zh/guides/common-tips.md b/docs/zh/guides/common-tips.md index 0c4b042ad..b9c54fb81 100644 --- a/docs/zh/guides/common-tips.md +++ b/docs/zh/guides/common-tips.md @@ -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()` 方法取回这些事件记录。 @@ -136,6 +180,106 @@ mount(Component, { _想查阅所有选项的完整列表,请移步该文档的[挂载选项](../api/options.md)章节。_ +### 仿造 Transitions + +尽管在大多数情况下使用 `await Vue.nextTick()` 效果很好,但是在某些情况下还需要一些额外的工作。这些问题将在 `vue-test-utils` 移出 beta 版本之前解决。其中一个例子是 Vue 提供的带有 `` 包装器的单元测试组件。 + +```vue + + + +``` + +您可能想编写一个测试用例来验证是否显示了文本 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 中 `` 组件的实现有关,我们希望在 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/) +}) +``` + +上面的代码重写了 `` 组件的默认行为,并在在条件发生变化时立即呈现子元素。这与 Vue 中 `` 组件应用 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`。 diff --git a/docs/zh/guides/getting-started.md b/docs/zh/guides/getting-started.md index b167545d2..01f54fcad 100644 --- a/docs/zh/guides/getting-started.md +++ b/docs/zh/guides/getting-started.md @@ -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 // 这不会被捕获 @@ -121,7 +135,7 @@ it('will time out', done => { }) }) -// 接下来的两项测试都会如预期工作 +// 接下来的三项测试都会如预期工作 it('will catch the error using done', done => { Vue.config.errorHandler = done Vue.nextTick(() => { @@ -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) +}) ``` ### 下一步是什么 diff --git a/docs/zh/guides/testing-async-components.md b/docs/zh/guides/testing-async-components.md index c1b2f4f12..00d813d9e 100644 --- a/docs/zh/guides/testing-async-components.md +++ b/docs/zh/guides/testing-async-components.md @@ -1,10 +1,41 @@ ## 测试异步行为 -为了让测试变得简单,`@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 { @@ -12,7 +43,7 @@ export default { } ``` -下面的组件在按钮被点击的时候会调用一个 API,然后将响应的值赋给 `value`。 +当按钮被点击时,组件将会产生一个 API 调用,并且将响应的返回内容赋值给 `value`。 ```html