Skip to content

Commit aa7b76d

Browse files
feat: return nextTick from setters, fix #1515 (#1517)
* feat: return promise from set methods * fix: fix error types for error wrapper * refactor: change promise assertion method * docs: mention that some methods can be awaited Generally improve async docs * fix: fix TS types * fix: fix nextTick for vue < 2.1 * docs: fix eslint error * chore: revert dist * chore: revert committed dist files Co-authored-by: Lachlan Miller <[email protected]>
1 parent 7a0b7e0 commit aa7b76d

15 files changed

+265
-209
lines changed

Diff for: docs/api/wrapper/trigger.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Triggers an event asynchronously on the `Wrapper` DOM node.
44

55
`trigger` takes an optional `options` object. The properties in the `options` object are added to the Event.
6+
`trigger` returns a Promise, which when resolved, guarantees the component is updated.
67

78
- **Arguments:**
89

@@ -22,18 +23,16 @@ test('trigger demo', async () => {
2223
propsData: { clickHandler }
2324
})
2425

25-
wrapper.trigger('click')
26+
await wrapper.trigger('click')
2627

27-
wrapper.trigger('click', {
28+
await wrapper.trigger('click', {
2829
button: 0
2930
})
3031

31-
wrapper.trigger('click', {
32+
await wrapper.trigger('click', {
3233
ctrlKey: true // For testing @click.ctrl handlers
3334
})
3435

35-
await wrapper.vm.$nextTick() // Wait until trigger events have been handled
36-
3736
expect(clickHandler.called).toBe(true)
3837
})
3938
```

Diff for: docs/guides/common-tips.md

+12-22
Original file line numberDiff line numberDiff line change
@@ -35,43 +35,35 @@ When using either the `mount` or `shallowMount` methods, you can expect your com
3535

3636
Additionally, the component will not be automatically destroyed at the end of each spec, and it is up to the user to stub or manually clean up tasks that will continue to run (`setInterval` or `setTimeout`, for example) before the end of each spec.
3737

38-
### Writing asynchronous tests using `nextTick` (new)
38+
### Writing asynchronous tests (new)
3939

4040
By default, Vue batches updates to run asynchronously (on the next "tick"). This is to prevent unnecessary DOM re-renders, and watcher computations ([see the docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue) for more details).
4141

42-
This means you **must** wait for updates to run after you set a reactive property. You can wait for updates with `Vue.nextTick()`:
42+
This means that you **must** wait for updates to run after you change a reactive property. You can do that by awaiting mutation methods like `trigger`:
4343

4444
```js
4545
it('updates text', async () => {
4646
const wrapper = mount(Component)
47-
wrapper.trigger('click')
48-
await Vue.nextTick()
47+
await wrapper.trigger('click')
4948
expect(wrapper.text()).toContain('updated')
49+
await wrapper.trigger('click')
50+
wrapper.text().toContain('some different text')
5051
})
5152

5253
// Or if you're without async/await
5354
it('render text', done => {
5455
const wrapper = mount(TestComponent)
55-
wrapper.trigger('click')
56-
Vue.nextTick(() => {
57-
wrapper.text().toContain('some text')
58-
wrapper.trigger('click')
59-
Vue.nextTick(() => {
56+
wrapper.trigger('click').then(() => {
57+
wrapper.text().toContain('updated')
58+
wrapper.trigger('click').then(() => {
6059
wrapper.text().toContain('some different text')
6160
done()
6261
})
6362
})
6463
})
6564
```
6665

67-
The following methods often cause watcher updates that require you to wait for the next tick:
68-
69-
- `setChecked`
70-
- `setData`
71-
- `setSelected`
72-
- `setProps`
73-
- `setValue`
74-
- `trigger`
66+
Learn more in the [Testing Asynchronous Behavior](../guides/README.md#testing-asynchronous-behavior)
7567

7668
### Asserting Emitted Events
7769

@@ -213,16 +205,15 @@ test('should render Foo, then hide it', async () => {
213205
const wrapper = mount(Foo)
214206
expect(wrapper.text()).toMatch(/Foo/)
215207

216-
wrapper.setData({
208+
await wrapper.setData({
217209
show: false
218210
})
219-
await wrapper.vm.$nextTick()
220211

221212
expect(wrapper.text()).not.toMatch(/Foo/)
222213
})
223214
```
224215

225-
In practice, although we are calling `setData` then waiting for the `nextTick` to ensure the DOM is updated, this test fails. This is an ongoing issue related to how Vue implements the `<transition>` component, that we would like to solve before version 1.0. For now, there are some workarounds:
216+
In practice, although we are calling and awaiting `setData` to ensure the DOM is updated, this test fails. This is an ongoing issue related to how Vue implements the `<transition>` component, that we would like to solve before version 1.0. For now, there are some workarounds:
226217

227218
#### Using a `transitionStub` helper
228219

@@ -241,10 +232,9 @@ test('should render Foo, then hide it', async () => {
241232
})
242233
expect(wrapper.text()).toMatch(/Foo/)
243234

244-
wrapper.setData({
235+
await wrapper.setData({
245236
show: false
246237
})
247-
await wrapper.vm.$nextTick()
248238

249239
expect(wrapper.text()).not.toMatch(/Foo/)
250240
})

Diff for: docs/guides/getting-started.md

+19-10
Original file line numberDiff line numberDiff line change
@@ -122,30 +122,34 @@ it('button click should increment the count', () => {
122122

123123
In order to test that the counter text has updated, we need to learn about `nextTick`.
124124

125-
### Using `nextTick`
125+
### Using `nextTick` and awaiting actions
126126

127-
Anytime you make a change (in computed, data, vuex state, etc) which updates the DOM (ex. show a component from v-if), you should await the `nextTick` function before running the test. This is because Vue batches pending DOM updates and _applies them asynchronously_ to prevent unnecessary re-renders caused by multiple data mutations.
127+
Anytime you make a change (in computed, data, vuex state, etc) which updates the DOM (ex. show a component from v-if or display dynamic text), you should await the `nextTick` function before running the assertion.
128+
This is because Vue batches pending DOM updates and _applies them asynchronously_ to prevent unnecessary re-renders caused by multiple data mutations.
128129

129130
_You can read more about asynchronous updates in the [Vue docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)_
130131

131-
We need to use `wrapper.vm.$nextTick` to wait until Vue has performed the DOM update after we set a reactive property. In the counter example, setting the `count` property schedules a DOM update to run on the next tick.
132+
After updating a reactive property we can await methods like `trigger` or `wrapper.vm.$nextTick` directly, until Vue has performed the DOM update. In the counter example, setting the `count` property schedules a DOM update to run on the next tick.
132133

133-
We can `await` `wrapper.vm.$nextTick()` by writing the tests in an async function:
134+
Lets see how we can `await trigger()` by writing the tests in an async function:
134135

135136
```js
136137
it('button click should increment the count text', async () => {
137138
expect(wrapper.text()).toContain('0')
138139
const button = wrapper.find('button')
139-
button.trigger('click')
140-
await wrapper.vm.$nextTick()
140+
await button.trigger('click')
141141
expect(wrapper.text()).toContain('1')
142142
})
143143
```
144144

145-
When you use `nextTick` in your test files, be aware that any errors thrown inside it may not be caught by your test runner as it uses promises internally. There are two approaches to fixing this: either you can set the `done` callback as Vue's global error handler at the start of the test, or you can call `nextTick` without an argument and return it as a promise:
145+
`trigger` returns a promise, which can be awaited as seen above or chained with `then` like a regular promise callback. Methods like `trigger` just return `Vue.nextTick` internally.
146+
You can read more in depth about [Testing Asynchronous Components](../guides/README.md#testing-async-components).
147+
148+
If for some reason you choose to use `nextTick` instead in your test files, be aware that any errors thrown inside it may not be caught by your test runner as it uses promises internally. There are two approaches to fixing this:
149+
either you can set the `done` callback as Vue's global error handler at the start of the test, or you can call `nextTick` without an argument and return it as a promise:
146150

147151
```js
148-
// this will not be caught
152+
// errors will not be caught
149153
it('will time out', done => {
150154
Vue.nextTick(() => {
151155
expect(true).toBe(false)
@@ -174,7 +178,12 @@ it('will catch the error using async/await', async () => {
174178
})
175179
```
176180

181+
`Vue.nextTick` is equal to `component.vm.$nextTick`, where `component` can be the result of `mount` or `find`.
182+
183+
As mentioned in the beginning, in most cases, awaiting `trigger` is the recommended way to go.
184+
177185
### What's Next
178186

179-
- Integrate Vue Test Utils into your project by [choosing a test runner](./choosing-a-test-runner.md).
180-
- Learn more about [common techniques when writing tests](./common-tips.md).
187+
- Learn more about [common techniques when writing tests](./README.md#knowing-what-to-test).
188+
- Integrate Vue Test Utils into your project by [choosing a test runner](./README.md#choosing-a-test-runner).
189+
- Learn more about [Testing Asynchronous Behavior](./README.md#testing-asynchronous-behavior)

Diff for: docs/guides/testing-async-components.md

+37-13
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ Vue batches pending DOM updates and applies them asynchronously to prevent unnec
1111

1212
_You can read more about asynchronous updates in the [Vue docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)_
1313

14-
In practice, this means you have to use `Vue.nextTick()` to wait until Vue has performed updates after you set a reactive property.
15-
16-
The easiest way to use `Vue.nextTick()` is to write your tests in an async function:
14+
In practice, this means that after mutating a reactive property, to assert that change your test has to wait while Vue is performing updates.
15+
One way is to use `await Vue.nextTick()`, but an easier and cleaner way is to just `await` the method that you mutated the state with, like `trigger`.
1716

1817
```js
19-
// import Vue at the top of file
20-
import Vue from 'vue'
18+
// inside test-suite, add this test case
19+
it('button click should increment the count text', async () => {
20+
expect(wrapper.text()).toContain('0')
21+
const button = wrapper.find('button')
22+
await button.trigger('click')
23+
expect(wrapper.text()).toContain('1')
24+
})
25+
```
2126

22-
// other code snippet...
27+
Awaiting the trigger above is the same as doing:
2328

24-
// inside test-suite, add this test case
29+
```js
2530
it('button click should increment the count text', async () => {
2631
expect(wrapper.text()).toContain('0')
2732
const button = wrapper.find('button')
@@ -31,6 +36,15 @@ it('button click should increment the count text', async () => {
3136
})
3237
```
3338

39+
Methods that can be awaited are:
40+
41+
- [setData](../api/wrapper/README.md#setdata)
42+
- [setValue](../api/wrapper/README.md#setvalue)
43+
- [setChecked](../api/wrapper/README.md#setchecked)
44+
- [setSelected](../api/wrapper/README.md#setselected)
45+
- [setProps](../api/wrapper/README.md#setprops)
46+
- [trigger](../api/wrapper/README.md#trigger)
47+
3448
## Asynchronous behavior outside of Vue
3549

3650
One of the most common asynchronous behaviors outside of Vue is API calls in Vuex actions. The following examples shows how to test a method that makes an API call. This example uses Jest to run the test and to mock the HTTP library `axios`. More about Jest manual mocks can be found [here](https://jestjs.io/docs/en/manual-mocks.html#content).
@@ -47,7 +61,7 @@ The below component makes an API call when a button is clicked, then assigns the
4761

4862
```html
4963
<template>
50-
<button @click="fetchResults" />
64+
<button @click="fetchResults">{{ value }}</button>
5165
</template>
5266

5367
<script>
@@ -75,12 +89,14 @@ A test can be written like this:
7589
```js
7690
import { shallowMount } from '@vue/test-utils'
7791
import Foo from './Foo'
78-
jest.mock('axios')
92+
jest.mock('axios', () => ({
93+
get: Promise.resolve('value')
94+
}))
7995

8096
it('fetches async when a button is clicked', () => {
8197
const wrapper = shallowMount(Foo)
8298
wrapper.find('button').trigger('click')
83-
expect(wrapper.vm.value).toBe('value')
99+
expect(wrapper.text()).toBe('value')
84100
})
85101
```
86102

@@ -91,15 +107,15 @@ it('fetches async when a button is clicked', done => {
91107
const wrapper = shallowMount(Foo)
92108
wrapper.find('button').trigger('click')
93109
wrapper.vm.$nextTick(() => {
94-
expect(wrapper.vm.value).toBe('value')
110+
expect(wrapper.text()).toBe('value')
95111
done()
96112
})
97113
})
98114
```
99115

100116
The reason `setTimeout` allows the test to pass is because the microtask queue where promise callbacks are processed runs before the task queue, where `setTimeout` callbacks are processed. This means by the time the `setTimeout` callback runs, any promise callbacks on the microtask queue will have been executed. `$nextTick` on the other hand schedules a microtask, but since the microtask queue is processed first-in-first-out that also guarantees the promise callback has been executed by the time the assertion is made. See [here](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) for a more detailed explanation.
101117

102-
Another solution is to use an `async` function and the [npm package flush-promises](https://www.npmjs.com/package/flush-promises). `flush-promises` flushes all pending resolved promise handlers. You can `await` the call of `flushPromises` to flush pending promises and improve the readability of your test.
118+
Another solution is to use an `async` function and a package like [flush-promises](https://www.npmjs.com/package/flush-promises). `flush-promises` flushes all pending resolved promise handlers. You can `await` the call of `flushPromises` to flush pending promises and improve the readability of your test.
103119

104120
The updated test looks like this:
105121

@@ -113,8 +129,16 @@ it('fetches async when a button is clicked', async () => {
113129
const wrapper = shallowMount(Foo)
114130
wrapper.find('button').trigger('click')
115131
await flushPromises()
116-
expect(wrapper.vm.value).toBe('value')
132+
expect(wrapper.text()).toBe('value')
117133
})
118134
```
119135

120136
This same technique can be applied to Vuex actions, which return a promise by default.
137+
138+
#### Why not just `await button.trigger()` ?
139+
140+
As explained above, there is a difference between the time it takes for Vue to update its components,
141+
and the time it takes for a Promise, like the one from `axios` to resolve.
142+
143+
A nice rule to follow is to always `await` on mutations like `trigger` or `setProps`.
144+
If your code relies on something async, like calling `axios`, add an await to the `flushPromises` call as well.

Diff for: flow/wrapper.flow.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ declare interface BaseWrapper {
3030
props(key?: string): { [name: string]: any } | any | void;
3131
text(): string | void;
3232
selector: Selector | void;
33-
setData(data: Object): void;
33+
setData(data: Object): Promise<void> | void;
3434
setMethods(methods: Object): void;
35-
setValue(value: any): void;
36-
setChecked(checked?: boolean): void;
37-
setSelected(): void;
38-
setProps(data: Object): void;
39-
trigger(type: string, options: Object): void;
35+
setValue(value: any): Promise<void> | void;
36+
setChecked(checked?: boolean): Promise<void> | void;
37+
setSelected(): Promise<void> | void;
38+
setProps(data: Object): Promise<void> | void;
39+
trigger(type: string, options: Object): Promise<void> | void;
4040
destroy(): void;
4141
}
4242

Diff for: packages/shared/util.js

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @flow
22
import Vue from 'vue'
33
import semver from 'semver'
4+
import { VUE_VERSION } from './consts'
45
import { config } from '@vue/test-utils'
56

67
export function throwError(msg: string): void {
@@ -87,6 +88,18 @@ export function getCheckedEvent() {
8788
return 'change'
8889
}
8990

91+
/**
92+
* Normalize nextTick to return a promise for all Vue 2 versions.
93+
* Vue < 2.1 does not return a Promise from nextTick
94+
* @return {Promise<R>}
95+
*/
96+
export function nextTick(): Promise<void> {
97+
if (VUE_VERSION > 2) return Vue.nextTick()
98+
return new Promise(resolve => {
99+
Vue.nextTick(resolve)
100+
})
101+
}
102+
90103
export function warnDeprecated(method: string, fallback: string = '') {
91104
if (!config.showDeprecationWarnings) return
92105
let msg = `${method} is deprecated and will removed in the next major version`

0 commit comments

Comments
 (0)